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

feat: serialize claim refresh calls #254

Merged
merged 3 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [20.0.1] - 2024-05-22

### Changes

- Now we use the locking to make sure that refreshing claims happens only once even for concurrent validateClaims calls
- The locking mechanism is configurable through by providing a `lockFactory` function in the configuration

## [20.0.0] - 2024-04-03

### Breaking changes
Expand Down
2 changes: 1 addition & 1 deletion bundle/bundle.js

Large diffs are not rendered by default.

119 changes: 90 additions & 29 deletions lib/build/recipeImplementation.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 44 additions & 7 deletions lib/ts/recipeImplementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { STGeneralError } from "./error";
import { addInterceptorsToXMLHttpRequest } from "./xmlhttprequest";
import { matchesDomainOrSubdomain, normaliseSessionScopeOrThrowError, normaliseURLDomainOrThrowError } from "./utils";
import DateProviderReference from "./utils/dateProvider";
import LockFactoryReference from "./utils/lockFactory";

const MAX_REFRESH_LOCK_TRY_COUNT = 100;
const CLAIM_REFRESH_LOCK_NAME = "CLAIM_REFRESH_LOCK";

export default function RecipeImplementation(recipeImplInput: {
preAPIHook: RecipePreAPIHookFunction;
Expand Down Expand Up @@ -223,7 +227,8 @@ export default function RecipeImplementation(recipeImplInput: {
claimValidators: SessionClaimValidator[];
userContext: any;
}): Promise<ClaimValidationError[]> {
let accessTokenPayload = await this.getAccessTokenPayloadSecurely({ userContext: input.userContext });
// We only load accessTokenPayload after acquiring the lock, since it could change until then
let accessTokenPayload;
// We first refresh all claims that may need to be refreshed, before running any validators,
// to avoid a situation where:
// 1. The payload passes claimValidators[0].
Expand All @@ -232,17 +237,49 @@ export default function RecipeImplementation(recipeImplInput: {
// 4. We return no errors since both claimValidators[0] and claimValidators[1] passed (but different states of the payload)
// Running all refreshes before validation avoids this.

for (const validator of input.claimValidators) {
if (await validator.shouldRefresh(accessTokenPayload, input.userContext)) {
let tryCount = 0;
while (++tryCount < MAX_REFRESH_LOCK_TRY_COUNT) {
const lockFactory = await LockFactoryReference.getReferenceOrThrow().lockFactory();
logDebugMessage("validateClaims: trying to acquire claim refresh lock");
const claimRefreshLock = await lockFactory.acquireLock(CLAIM_REFRESH_LOCK_NAME);
if (claimRefreshLock) {
accessTokenPayload = await this.getAccessTokenPayloadSecurely({ userContext: input.userContext });
logDebugMessage("validateClaims: claim refresh lock acquired");
// to sync across tabs. the 1000 ms wait is for how much time to try and acquire the lock
try {
await validator.refresh(input.userContext);
} catch (err) {
console.error(`Encountered an error while refreshing validator ${validator.id}`, err);
for (const validator of input.claimValidators) {
if (await validator.shouldRefresh(accessTokenPayload, input.userContext)) {
try {
await validator.refresh(input.userContext);
} catch (err) {
console.error(
`Encountered an error while refreshing validator ${validator.id}`,
err
);
}
accessTokenPayload = await this.getAccessTokenPayloadSecurely({
userContext: input.userContext
});
}
}
} finally {
logDebugMessage("validateClaims: releasing claim refresh lock");
await lockFactory.releaseLock(CLAIM_REFRESH_LOCK_NAME);
}
accessTokenPayload = await this.getAccessTokenPayloadSecurely({ userContext: input.userContext });
break;
} else {
logDebugMessage(`validateClaims: Retrying refresh lock ${tryCount}/${MAX_REFRESH_LOCK_TRY_COUNT}`);
}
}

if (tryCount === MAX_REFRESH_LOCK_TRY_COUNT) {
logDebugMessage("validateClaims: ran out of retries while trying to acquire claim refresh lock");
// We can just load the access token payload (that doesn't happen above if we never got inside the lock)
accessTokenPayload = await this.getAccessTokenPayloadSecurely({ userContext: input.userContext });
// and let the claim validation proceed. This matches our behaviour of letting the validation proceed
// even if a refresh function threw or failed to refresh a claim.
}

const errors = [];
for (const validator of input.claimValidators) {
const validationRes = await validator.validate(accessTokenPayload, input.userContext);
Expand Down
2 changes: 1 addition & 1 deletion lib/ts/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
export const package_version = "20.0.0";
export const package_version = "20.0.1";

export const supported_fdi = ["1.16", "1.17", "1.18", "1.19"];
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supertokens-website",
"version": "20.0.0",
"version": "20.0.1",
"description": "frontend sdk for website to be used for auth solution.",
"main": "index.js",
"dependencies": {
Expand Down
Loading
Loading