UNPKG

convex

Version:

Client for the Convex Cloud

250 lines (249 loc) 7.78 kB
"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