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

MNTOR-3353: add refresh token logic to session management #4777

Merged
merged 30 commits into from
Jul 18, 2024
Merged

Conversation

mansaj
Copy link
Collaborator

@mansaj mansaj commented Jul 8, 2024

References:

Jira: MNTOR-3353

Description

  • Adding refresh token fetching logic to next-auth
  • The expiry / tokens are stored in the db

Test

Test for refresh token

  • run db:migration and make sure the fxa_session_expiry col exists
  • If the column is empty, login and see that the session expiry should be an hour in the future
  • Change the time to be some time in the past OR set it to null
  • Go to dashboard, watch the session refresh, come back to the table and check on the session field again, it should have refreshed
  • The access token should have also been refreshed

Test for refresh fail

  • empty out the columns fxa_session_expiry, fxa_access_token, and fxa_refresh_token columns in the subscribers table
  • navigate to either dashboard or settings
  • it should automatically log the user out and prompt a relogin

Checklist (Definition of Done)

  • Commits in this PR are minimal and have descriptive commit messages.
  • I've added or updated the relevant sections in readme and/or code comments
  • All acceptance criteria are met.
  • Jira ticket has been updated (if needed) to match changes made during the development process.
  • Jira ticket has been updated (if needed) with suggestions for QA when this PR is deployed to stage.
@mansaj mansaj requested review from rhelmer and Vinnl July 8, 2024 18:53
@mansaj
Copy link
Collaborator Author

mansaj commented Jul 8, 2024

One thing I'm trying to figure out is where to add the client side of the session error to force re-sign in.
I'm thinking maybe in the dashboard (and settings), have a useSession and try to detect that error. If I see the error, then redirect?

src/app/api/utils/auth.ts Outdated Show resolved Hide resolved
src/app/api/utils/auth.ts Show resolved Hide resolved
src/app/api/utils/auth.ts Outdated Show resolved Hide resolved
src/db/tables/subscribers.js Outdated Show resolved Hide resolved
src/utils/fxa.js Show resolved Hide resolved
src/utils/fxa.js Outdated Show resolved Hide resolved
Copy link
Collaborator

@Vinnl Vinnl left a comment

Choose a reason for hiding this comment

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

Could you add a "How to test" section? I'm unsure how to tell now whether this works as intended.

The most important thing that I think needs changing is doing this check when constructing the JWT, rather than when validating the session, since the latter happens pretty often.

src/app/api/utils/auth.ts Outdated Show resolved Hide resolved
src/db/tables/subscribers.js Outdated Show resolved Hide resolved
*/
// Not covered by tests; mostly side-effects. See test-coverage.md#mock-heavy
/* c8 ignore start */
async function updateFxATokens (subscriber, fxaAccessToken, fxaRefreshToken, sessionExpiredAt) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a bit confused by the significant overlap between this function and updateFxAData - is that one used in similar situations as this one? Should it be? It's also calling destroyOAuthToken, should this function do that too?

Could you add some comments to the code to prevent such confusion from future readers?

src/db/tables/emailAddresses.js Outdated Show resolved Hide resolved
src/app/api/utils/auth.ts Outdated Show resolved Hide resolved
const dbFxATokens = await getFxATokens(token.subscriber.id);
if (
!dbFxATokens.fxa_session_expiry ||
dbFxATokens.fxa_session_expiry.getTime() < Date.now()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we should try rotating it if it expires soon, rather than when it has already expired? Since sessions have a lifetime as well in which the token can expire.

Suggested change
dbFxATokens.fxa_session_expiry.getTime() < Date.now()
dbFxATokens.fxa_session_expiry.getTime() < (Date.now() - 5 * 60 * 1000)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't know if we should artificially set a time (not really a best practice)
I think the point is to let sessions expire and potentially force a refresh or relogin

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess the question is: how do we do that? Are we going to refresh the session every time an API request to FxA fails with the relevant status code? Since this callback won't necessarily automatically get called when the session expires.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess the question is: how do we do that? Are we going to refresh the session every time an API request to FxA fails with the relevant status code? Since this callback won't necessarily automatically get called when the session expires.

Yes I am wondering that too... I'm not sure what is going to trigger this session callback if e.g. a fetch to some API (like subscriptions details) fails.

@@ -209,7 +213,7 @@ export const authOptions: AuthOptions = {
}
return token;
},
session({ session, token }) {
async session({ session, token }) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't refreshing the access token happen in the jwt callback? As I understand it, the session callback gets called pretty often, so hitting the database there can cause pretty heavy load. And the JWT would still contain the expired access token.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So there are two approaches to this. The database route is a bit heavy, but it's also a bit safer. It's outlined in the next-auth doc.

We've traditionally opted for storing the sessions etc the database (prior to moving to next-auth). We were also using redis for session management. I think it should be an either-or situation

Copy link
Collaborator

@Vinnl Vinnl Jul 10, 2024

Choose a reason for hiding this comment

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

Oh yeah, I'm not saying not going the database route, but doing it less often? I think the session gets refreshed whenever the user switches tabs, or about every minute when the user has a tab open, at least when an app is using the JWT session strategy - which Monitor does.

We could consider switching to the database session strategy, in which case I think the session callback will be called less often and doing the refresh there makes sense, but if we don't do that, I'd be afraid we'll be hammering our DB too much. But I'm not a back-end engineer, so I'll defer to your judgment :)

Copy link
Collaborator

@rhelmer rhelmer left a comment

Choose a reason for hiding this comment

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

Sounds like from talking this morning that you're going to investigate switching over to the database strategy - I think that will make things a lot clearer, since we're sort-of using both strategies right now.

const dbFxATokens = await getFxATokens(token.subscriber.id);
if (
!dbFxATokens.fxa_session_expiry ||
dbFxATokens.fxa_session_expiry.getTime() < Date.now()
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess the question is: how do we do that? Are we going to refresh the session every time an API request to FxA fails with the relevant status code? Since this callback won't necessarily automatically get called when the session expires.

Yes I am wondering that too... I'm not sure what is going to trigger this session callback if e.g. a fetch to some API (like subscriptions details) fails.

@mansaj mansaj requested a review from rhelmer July 15, 2024 22:30
src/app/functions/server/applyCoupon.ts Outdated Show resolved Hide resolved
src/app/functions/server/checkSession.ts Outdated Show resolved Hide resolved
src/utils/fxa.js Show resolved Hide resolved
@mansaj
Copy link
Collaborator Author

mansaj commented Jul 16, 2024

@rhelmer one thing we'd want to do in stage is to load test this for database connection (we will be checking db pretty frequently with the session changes). I'm optimistic that most of it should just be get and not update but it'd be nice to see

@mansaj mansaj requested a review from rhelmer July 16, 2024 16:36
@mansaj mansaj merged commit fe5d69e into main Jul 18, 2024
15 checks passed
@mansaj mansaj deleted the MNTOR-3353 branch July 18, 2024 22:38
Copy link

Cleanup completed - database 'blurts-server-pr-4777' destroyed, cloud run service 'blurts-server-pr-4777' destroyed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
4 participants