@aws-amplify/auth
Version:
Auth category of aws-amplify
209 lines (207 loc) • 9.33 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.TokenOrchestrator = void 0;
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
const core_1 = require("@aws-amplify/core");
const utils_1 = require("@aws-amplify/core/internals/utils");
const assertServiceError_1 = require("../../../errors/utils/assertServiceError");
const AuthError_1 = require("../../../errors/AuthError");
const oAuthStore_1 = require("../utils/oauth/oAuthStore");
const inflightPromise_1 = require("../utils/oauth/inflightPromise");
class TokenOrchestrator {
constructor() {
this.waitForInflightOAuth = (0, utils_1.isBrowser)()
? async () => {
// Read-time evaluation of the blocking deadline: absent flag, an
// expired deadline, or a flag value other than 'true' all mean
// "do not block". An abandoned flow in another tab can therefore
// never park token consumers indefinitely.
// (`loadOAuthInFlightDeadline` is optional on the OAuthStore
// interface for custom-implementation compatibility, but this
// singleton is always the concrete DefaultOAuthStore, which
// implements it.)
const deadline = await oAuthStore_1.oAuthStore.loadOAuthInFlightDeadline();
if (deadline === undefined) {
return;
}
if (this.inflightPromise) {
// Keep the backstop aligned with the current deadline for waiters
// piggybacking on the existing park.
(0, inflightPromise_1.armInflightDeadline)(deadline, () => oAuthStore_1.oAuthStore.loadOAuthInFlightDeadline());
return this.inflightPromise;
}
// when there is valid oauth config and there is an inflight oauth flow, try
// to block async calls that require fetching tokens before the oauth flow completes
// e.g. getCurrentUser, fetchAuthSession etc.
this.inflightPromise = new Promise(resolve => {
// Invariant: `this.inflightPromise` is owned by the park lifecycle —
// created here and reset by the very resolver that releases it
// (drained by the backstop timer, the cross-tab listener, or
// in-process completion). Resetting BEFORE resolving closes the
// post-release hole where a caller for a NEW flow could observe a
// stale, already-resolved promise and skip blocking on it.
(0, inflightPromise_1.addInflightPromise)(() => {
this.inflightPromise = undefined;
resolve();
});
// Arm the deadline backstop in the same synchronous step as the
// park: it releases this waiter even when the cross-tab release
// fired between the deadline read above and this park (that storage
// event never re-fires), when storage events are unavailable
// (Safari private mode), or when the flow is simply never
// completed anywhere.
(0, inflightPromise_1.armInflightDeadline)(deadline, () => oAuthStore_1.oAuthStore.loadOAuthInFlightDeadline());
});
return this.inflightPromise;
}
: async () => {
// no-op for non-browser environments
};
}
setAuthConfig(authConfig) {
oAuthStore_1.oAuthStore.setAuthConfig(authConfig.Cognito);
this.authConfig = authConfig;
}
setTokenRefresher(tokenRefresher) {
this.tokenRefresher = tokenRefresher;
}
setAuthTokenStore(tokenStore) {
this.tokenStore = tokenStore;
}
getTokenStore() {
if (!this.tokenStore) {
throw new AuthError_1.AuthError({
name: 'EmptyTokenStoreException',
message: 'TokenStore not set',
});
}
return this.tokenStore;
}
getTokenRefresher() {
if (!this.tokenRefresher) {
throw new AuthError_1.AuthError({
name: 'EmptyTokenRefresherException',
message: 'TokenRefresher not set',
});
}
return this.tokenRefresher;
}
setClientMetadataProvider(clientMetadataProvider) {
this.clientMetadataProvider = clientMetadataProvider;
}
async getTokens(options) {
let tokens;
try {
(0, utils_1.assertTokenProviderConfig)(this.authConfig?.Cognito);
}
catch (_err) {
// Token provider not configured
return null;
}
await this.waitForInflightOAuth();
// NOTE: `this.inflightPromise` is reset by the resolver registered in
// `waitForInflightOAuth` (co-located with the release), NOT here — a reset
// here could clobber a newer flow's park created between the release and
// this line resuming.
tokens = await this.getTokenStore().loadTokens();
const username = await this.getTokenStore().getLastAuthUser();
if (tokens === null) {
return null;
}
const idTokenExpired = !!tokens?.idToken &&
(0, utils_1.isTokenExpired)({
expiresAt: (tokens.idToken?.payload?.exp ?? 0) * 1000,
clockDrift: tokens.clockDrift ?? 0,
});
const accessTokenExpired = (0, utils_1.isTokenExpired)({
expiresAt: (tokens.accessToken?.payload?.exp ?? 0) * 1000,
clockDrift: tokens.clockDrift ?? 0,
});
if (options?.forceRefresh || idTokenExpired || accessTokenExpired) {
tokens = await this.refreshTokens({
tokens,
username,
clientMetadata: options?.clientMetadata ?? (await this.clientMetadataProvider?.()),
});
if (tokens === null) {
return null;
}
}
return {
accessToken: tokens?.accessToken,
idToken: tokens?.idToken,
signInDetails: tokens?.signInDetails,
};
}
async refreshTokens({ tokens, username, clientMetadata, }) {
try {
const { signInDetails } = tokens;
const newTokens = await this.getTokenRefresher()({
tokens,
authConfig: this.authConfig,
username,
clientMetadata,
});
newTokens.signInDetails = signInDetails;
await this.setTokens({ tokens: newTokens });
core_1.Hub.dispatch('auth', { event: 'tokenRefresh' }, 'Auth', utils_1.AMPLIFY_SYMBOL);
return newTokens;
}
catch (err) {
return this.handleErrors(err);
}
}
handleErrors(err) {
(0, assertServiceError_1.assertServiceError)(err);
// Only clear tokens for definitive authentication failures
// Do NOT clear tokens for transient errors like service issues, rate limits, etc.
const shouldClearTokens = this.isAuthenticationError(err);
if (shouldClearTokens) {
this.clearTokens();
}
core_1.Hub.dispatch('auth', {
event: 'tokenRefresh_failure',
data: { error: err },
}, 'Auth', utils_1.AMPLIFY_SYMBOL);
if (err.name.startsWith('NotAuthorizedException')) {
return null;
}
throw err;
}
isAuthenticationError(err) {
// Only clear tokens for errors that definitively indicate the tokens are invalid
// and re-authentication is required. All other errors (service errors, rate limits, etc.)
// should preserve the tokens to allow for retry.
// See: https://github.com/aws-amplify/amplify-js/issues/14534
const authErrorNames = [
'NotAuthorizedException', // Refresh token is expired or invalid
'TokenRevokedException', // Token was revoked by admin
'UserNotFoundException', // User no longer exists
'PasswordResetRequiredException', // User must reset password
'UserNotConfirmedException', // User account is not confirmed
'RefreshTokenReuseException', // Refresh token invalidated by rotation
];
return authErrorNames.some(errorName => err?.name?.startsWith?.(errorName));
}
async setTokens({ tokens }) {
return this.getTokenStore().storeTokens(tokens);
}
async clearTokens() {
return this.getTokenStore().clearTokens();
}
getDeviceMetadata(username) {
return this.getTokenStore().getDeviceMetadata(username);
}
clearDeviceMetadata(username) {
return this.getTokenStore().clearDeviceMetadata(username);
}
setOAuthMetadata(metadata) {
return this.getTokenStore().setOAuthMetadata(metadata);
}
getOAuthMetadata() {
return this.getTokenStore().getOAuthMetadata();
}
}
exports.TokenOrchestrator = TokenOrchestrator;
//# sourceMappingURL=TokenOrchestrator.js.map