convex
Version:
Client for the Convex Cloud
250 lines (249 loc) • 7.78 kB
JavaScript
"use strict";
import jwtDecode from "jwt-decode";
export class AuthenticationManager {
constructor(syncState, {
authenticate,
pauseSocket: pause,
resumeSocket: resume,
clearAuth,
verbose
}) {
this.authState = { state: "noAuth" };
// Used to detect races involving `setConfig` calls
// while a token is being fetched.
this.configVersion = 0;
this.syncState = syncState;
this.authenticate = authenticate;
this.pauseSocket = pause;
this.resumeSocket = resume;
this.clearAuth = clearAuth;
this.verbose = verbose;
}
async setConfig(fetchToken, onChange) {
this.resetAuthState();
const token = await this.fetchTokenAndGuardAgainstRace(fetchToken, {
forceRefreshToken: false
});
if (token.isFromOutdatedConfig) {
return;
}
if (token.value) {
this.setAuthState({
state: "waitingForServerConfirmationOfCachedToken",
config: { fetchToken, onAuthChange: onChange },
hasRetried: false
});
this.authenticate(token.value);
} else {
this.setAuthState({
state: "initialRefetch",
config: { fetchToken, onAuthChange: onChange }
});
await this.refetchToken();
}
}
onTransition(serverMessage) {
if (!this.syncState.isCurrentOrNewerAuthVersion(
serverMessage.endVersion.identity
)) {
return;
}
if (serverMessage.endVersion.identity <= serverMessage.startVersion.identity) {
return;
}
if (this.authState.state === "waitingForServerConfirmationOfCachedToken") {
this._logVerbose("server confirmed auth token is valid");
void this.refetchToken();
this.authState.config.onAuthChange(true);
return;
}
if (this.authState.state === "waitingForServerConfirmationOfFreshToken") {
this._logVerbose("server confirmed new auth token is valid");
this.scheduleTokenRefetch(this.authState.token);
if (!this.authState.hadAuth) {
this.authState.config.onAuthChange(true);
}
}
}
onAuthError(serverMessage) {
const { baseVersion } = serverMessage;
if (baseVersion !== null && baseVersion !== void 0) {
if (!this.syncState.isCurrentOrNewerAuthVersion(baseVersion + 1)) {
this._logVerbose("ignoring auth error for previous auth attempt");
return;
}
void this.tryToReauthenticate(serverMessage);
return;
}
void this.tryToReauthenticate(serverMessage);
}
// This is similar to `refetchToken` defined below, in fact we
// don't represent them as different states, but it is different
// in that we pause the WebSocket so that mutations
// don't retry with bad auth.
async tryToReauthenticate(serverMessage) {
if (
// No way to fetch another token, kaboom
this.authState.state === "noAuth" || // We failed on a fresh token, trying another one won't help
this.authState.state === "waitingForServerConfirmationOfFreshToken"
) {
console.error(
`Failed to authenticate: "${serverMessage.error}", check your server auth config`
);
if (this.syncState.hasAuth()) {
this.syncState.clearAuth();
}
if (this.authState.state !== "noAuth") {
this.setAndReportAuthFailed(this.authState.config.onAuthChange);
}
return;
}
this._logVerbose("attempting to reauthenticate");
await this.pauseSocket();
const token = await this.fetchTokenAndGuardAgainstRace(
this.authState.config.fetchToken,
{
forceRefreshToken: true
}
);
if (token.isFromOutdatedConfig) {
await this.resumeSocket();
return;
}
if (token.value && this.syncState.isNewAuth(token.value)) {
this.syncState.setAuth(token.value);
this.setAuthState({
state: "waitingForServerConfirmationOfFreshToken",
config: this.authState.config,
token: token.value,
hadAuth: this.authState.state === "notRefetching" || this.authState.state === "waitingForScheduledRefetch"
});
} else {
this._logVerbose("reauthentication failed, could not fetch a new token");
if (this.syncState.hasAuth()) {
this.syncState.clearAuth();
}
this.setAndReportAuthFailed(this.authState.config.onAuthChange);
}
await this.resumeSocket();
}
// Force refetch the token and schedule another refetch
// before the token expires - an active client should never
// need to reauthenticate.
async refetchToken() {
if (this.authState.state === "noAuth") {
return;
}
this._logVerbose("refetching auth token");
const token = await this.fetchTokenAndGuardAgainstRace(
this.authState.config.fetchToken,
{
forceRefreshToken: true
}
);
if (token.isFromOutdatedConfig) {
return;
}
if (token.value) {
if (this.syncState.isNewAuth(token.value)) {
this.setAuthState({
state: "waitingForServerConfirmationOfFreshToken",
hadAuth: this.syncState.hasAuth(),
token: token.value,
config: this.authState.config
});
this.authenticate(token.value);
} else {
this.setAuthState({
state: "notRefetching",
config: this.authState.config
});
}
} else {
this._logVerbose("refetching token failed");
if (this.syncState.hasAuth()) {
this.clearAuth();
}
this.setAndReportAuthFailed(this.authState.config.onAuthChange);
}
}
scheduleTokenRefetch(token) {
if (this.authState.state === "noAuth") {
return;
}
const decodedToken = this.decodeToken(token);
if (!decodedToken) {
console.error("Auth token is not a valid JWT, cannot refetch the token");
return;
}
const { iat, exp } = decodedToken;
if (!iat || !exp) {
console.error(
"Auth token does not have required fields, cannot refetch the token"
);
return;
}
const leewaySeconds = 2;
const delay = (exp - iat - leewaySeconds) * 1e3;
if (delay <= 0) {
console.error(
"Auth token does not live long enough, cannot refetch the token"
);
return;
}
const refetchTokenTimeoutId = setTimeout(() => {
void this.refetchToken();
}, delay);
this.setAuthState({
state: "waitingForScheduledRefetch",
refetchTokenTimeoutId,
config: this.authState.config
});
this._logVerbose(
`scheduled preemptive auth token refetching in ${delay}ms`
);
}
// Protects against simultaneous calls to `setConfig`
// while we're fetching a token
async fetchTokenAndGuardAgainstRace(fetchToken, fetchArgs) {
const originalConfigVersion = ++this.configVersion;
const token = await fetchToken(fetchArgs);
if (this.configVersion !== originalConfigVersion) {
return { isFromOutdatedConfig: true };
}
return { isFromOutdatedConfig: false, value: token };
}
stop() {
this.resetAuthState();
this.configVersion++;
}
setAndReportAuthFailed(onAuthChange) {
onAuthChange(false);
this.resetAuthState();
}
resetAuthState() {
this.setAuthState({ state: "noAuth" });
}
setAuthState(newAuth) {
if (this.authState.state === "waitingForScheduledRefetch") {
clearTimeout(this.authState.refetchTokenTimeoutId);
this.syncState.markAuthCompletion();
}
this.authState = newAuth;
}
decodeToken(token) {
try {
return jwtDecode(token);
} catch (e) {
return null;
}
}
_logVerbose(message) {
if (this.verbose) {
console.debug(
`${(/* @__PURE__ */ new Date()).toISOString()} ${message} [v${this.configVersion}]`
);
}
}
}
//# sourceMappingURL=authentication_manager.js.map