-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
authn: Support marking user data as "stale before" a timestamp
This is a basic cache-invalidation system applied to our authn tokens (and future snapshots of user info).¹ When the app knowingly makes changes to user data in Cognito, such as modifying a user's group memberships, it can mark user data as "stale before" that time. For sessions, this triggers automatic server-side token renewal and thus refresh of user data (e.g. the new group memberships) from Cognito. For API clients (like Nextstrain CLI), this triggers a 401 Unauthorized error prompting the client to retry after renewal. Timestamps are stored in Redis under a per-user key, if Redis is available, else they're stored in a transient in-process Map (useful only for dev). Currently no code sets these timestamps. I expect the first usage to be with the addition of group membership management endpoints to the RESTful API. Partially resolves <#81>. ¹ Using JWTs means you're dealing with a cache (e.g. of the data in the token), even if you don't normally think about it that way. While it might seem we could use the JWT to just get the username and always fetch the latest user info from Cognito's admin API, this isn't feasible because of the rate limits imposed by Cognito and the additional latency it would introduce.
- Loading branch information
Showing
4 changed files
with
118 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* User management. | ||
* | ||
* @module user | ||
*/ | ||
|
||
const {REDIS} = require("./redis"); | ||
|
||
|
||
/** | ||
* Transient, in-memory "store" for userStaleBefore timestamps when Redis isn't | ||
* available (e.g. in local dev). | ||
*/ | ||
const userStaleBeforeTransientStore = new Map(); | ||
|
||
|
||
/** | ||
* Get the "stale before" timestamp for the given user. | ||
* | ||
* User tokens issued before this timestamp should be considered stale: their | ||
* claims may not reflect the most current user information and they should be | ||
* renewed. | ||
* | ||
* @param {String} username | ||
* @returns {Number|null} timestamp | ||
* @see markUserStaleBeforeNow | ||
*/ | ||
async function userStaleBefore(username) { | ||
const timestamp = REDIS | ||
? parseInt(await REDIS.get(`userStaleBefore:${username}`), 10) | ||
: userStaleBeforeTransientStore.get(username); | ||
|
||
return Number.isInteger(timestamp) | ||
? timestamp | ||
: null; | ||
} | ||
|
||
|
||
/** | ||
* Set the "stale before" timestamp for the given user to the current time. | ||
* | ||
* This timestamp should be set whenever the app makes a change to a user's | ||
* information in Cognito, such as modifying a user's group memberships or | ||
* updating a user's email address. | ||
* | ||
* @param {String} username | ||
* @returns {Boolean} True if set succeeded; false if not. | ||
* @see userStaleBefore | ||
*/ | ||
async function markUserStaleBeforeNow(username) { | ||
const now = Math.ceil(Date.now() / 1000); | ||
if (REDIS) { | ||
/* The TTL must be greater than the maximum lifetime of an id/access token | ||
* or any other cached user data. AWS Cognito limits the maximum lifetime | ||
* of id/access tokens to 24 hours, and those tokens are the only cached | ||
* user data we're using this timestamp for currently. Set expiration to | ||
* 25 hours to avoid any clock jitter issues. | ||
* -trs, 10 May 2022 | ||
*/ | ||
const result = await REDIS.set(`userStaleBefore:${username}`, now, "EX", 25 * 60 * 60); | ||
return result === "OK"; | ||
} | ||
userStaleBeforeTransientStore.set(username, now); | ||
return true; | ||
} | ||
|
||
|
||
module.exports = { | ||
userStaleBefore, | ||
markUserStaleBeforeNow, | ||
}; |