ag-awsauth 0.0.293 → 0.0.295
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -1
- package/dist/helpers/awsconfig.d.ts +9 -0
- package/dist/helpers/awsconfig.js +46 -16
- package/dist/helpers/browser.js +48 -28
- package/dist/helpers/credentialsLock.d.ts +12 -0
- package/dist/helpers/credentialsLock.js +102 -0
- package/dist/helpers/oidcRefresh.d.ts +14 -0
- package/dist/helpers/oidcRefresh.js +69 -8
- package/dist/helpers/sso.js +16 -15
- package/dist/index.js +24 -9
- package/package.json +12 -9
package/dist/config.d.ts
CHANGED
|
@@ -11,3 +11,6 @@ export declare const runConfig: () => void;
|
|
|
11
11
|
export declare const timeoutMs = 30000;
|
|
12
12
|
export declare const timeoutShortMs = 3000;
|
|
13
13
|
export declare const timeoutMedMs = 6000;
|
|
14
|
+
export declare const refreshWindowMs: number;
|
|
15
|
+
export declare const credentialsLockTimeoutMs: number;
|
|
16
|
+
export declare const credentialsLockStaleMs: number;
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.timeoutMedMs = exports.timeoutShortMs = exports.timeoutMs = exports.runConfig = exports.setConfig = exports.validateConfig = exports.targetRegion = exports.ssoStartUrl = exports.identityCenterRegion = exports.setBasePath = exports.basePath = exports.stsDurationSeconds = exports.logPath = void 0;
|
|
6
|
+
exports.credentialsLockStaleMs = exports.credentialsLockTimeoutMs = exports.refreshWindowMs = exports.timeoutMedMs = exports.timeoutShortMs = exports.timeoutMs = exports.runConfig = exports.setConfig = exports.validateConfig = exports.targetRegion = exports.ssoStartUrl = exports.identityCenterRegion = exports.setBasePath = exports.basePath = exports.stsDurationSeconds = exports.logPath = void 0;
|
|
7
7
|
const envfile_1 = require("envfile");
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -53,3 +53,10 @@ exports.runConfig = runConfig;
|
|
|
53
53
|
exports.timeoutMs = 30000;
|
|
54
54
|
exports.timeoutShortMs = 3000;
|
|
55
55
|
exports.timeoutMedMs = 6000;
|
|
56
|
+
// Refresh the OIDC access token this far ahead of expiry so the rotating
|
|
57
|
+
// refresh-token chain stays alive instead of being used lazily after a 401.
|
|
58
|
+
exports.refreshWindowMs = 5 * 60 * 1000;
|
|
59
|
+
// Serialise credential file updates across processes. A refresh token is
|
|
60
|
+
// single-use, so two concurrent runs must never refresh at the same time.
|
|
61
|
+
exports.credentialsLockTimeoutMs = 10 * 1000;
|
|
62
|
+
exports.credentialsLockStaleMs = 30 * 1000;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import type { IAwsCreds, IAwsCredsRaw } from '../types';
|
|
2
2
|
export declare const getAwsCredentials: () => Promise<Record<string, IAwsCredsRaw>>;
|
|
3
|
+
export declare const readAwsCredentials: () => Promise<IAwsCreds | undefined>;
|
|
4
|
+
/**
|
|
5
|
+
* Read-modify-write of the credentials file. Callers must already hold the
|
|
6
|
+
* credentials lock (use `updateAwsCredentials` unless you are inside one).
|
|
7
|
+
*
|
|
8
|
+
* OIDC refresh material is preserved unless explicitly replaced: a partial
|
|
9
|
+
* credentials object must never wipe the only token that avoids a full login.
|
|
10
|
+
*/
|
|
11
|
+
export declare const saveAwsCredentialsUnlocked: (p: IAwsCreds | undefined) => Promise<void>;
|
|
3
12
|
export declare const updateAwsCredentials: (p: IAwsCreds | undefined) => Promise<void>;
|
|
@@ -12,15 +12,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.updateAwsCredentials = exports.getAwsCredentials = void 0;
|
|
15
|
+
exports.updateAwsCredentials = exports.saveAwsCredentialsUnlocked = exports.readAwsCredentials = exports.getAwsCredentials = void 0;
|
|
16
16
|
const shared_ini_file_loader_1 = require("@aws-sdk/shared-ini-file-loader");
|
|
17
17
|
const log_1 = require("ag-common/dist/common/helpers/log");
|
|
18
18
|
const fs_1 = __importDefault(require("fs"));
|
|
19
19
|
const ini_1 = require("ini");
|
|
20
20
|
const path_1 = require("path");
|
|
21
|
+
const config_1 = require("../config");
|
|
22
|
+
const credentialsLock_1 = require("./credentialsLock");
|
|
21
23
|
const getCredentialsFilepath_1 = require("./getCredentialsFilepath");
|
|
22
24
|
const getAwsCredentials = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
-
|
|
25
|
+
// Read fresh from disk: the SDK caches file contents per path, which hides
|
|
26
|
+
// tokens rotated by another process (or by an earlier step of this run).
|
|
27
|
+
const config = yield (0, shared_ini_file_loader_1.loadSharedConfigFiles)({ ignoreCache: true });
|
|
24
28
|
const creds = config.credentialsFile;
|
|
25
29
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
26
30
|
if (!creds.default) {
|
|
@@ -31,7 +35,34 @@ const getAwsCredentials = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
31
35
|
return ret;
|
|
32
36
|
});
|
|
33
37
|
exports.getAwsCredentials = getAwsCredentials;
|
|
34
|
-
const
|
|
38
|
+
const readAwsCredentials = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
const credraw = yield (0, exports.getAwsCredentials)();
|
|
40
|
+
if (!credraw.default.aws_access_token) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
accessKeyId: credraw.default.aws_access_key_id,
|
|
45
|
+
secretAccessKey: credraw.default.aws_secret_access_key,
|
|
46
|
+
sessionToken: credraw.default.aws_session_token,
|
|
47
|
+
accessToken: credraw.default.aws_access_token,
|
|
48
|
+
ssoAuthn: credraw.default.aws_sso_authn,
|
|
49
|
+
refreshToken: credraw.default.aws_sso_refresh_token,
|
|
50
|
+
clientId: credraw.default.aws_sso_client_id,
|
|
51
|
+
clientSecret: credraw.default.aws_sso_client_secret,
|
|
52
|
+
clientSecretExpiresAt: Number(credraw.default.aws_sso_client_secret_expires_at) || undefined,
|
|
53
|
+
accessTokenExpiresAt: Number(credraw.default.aws_sso_access_token_expires_at) || undefined,
|
|
54
|
+
region: config_1.identityCenterRegion,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
exports.readAwsCredentials = readAwsCredentials;
|
|
58
|
+
/**
|
|
59
|
+
* Read-modify-write of the credentials file. Callers must already hold the
|
|
60
|
+
* credentials lock (use `updateAwsCredentials` unless you are inside one).
|
|
61
|
+
*
|
|
62
|
+
* OIDC refresh material is preserved unless explicitly replaced: a partial
|
|
63
|
+
* credentials object must never wipe the only token that avoids a full login.
|
|
64
|
+
*/
|
|
65
|
+
const saveAwsCredentialsUnlocked = (p) => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
66
|
const creds = yield (0, exports.getAwsCredentials)();
|
|
36
67
|
if (!p) {
|
|
37
68
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -44,26 +75,23 @@ const updateAwsCredentials = (p) => __awaiter(void 0, void 0, void 0, function*
|
|
|
44
75
|
creds.default.aws_session_token = p.sessionToken;
|
|
45
76
|
creds.default.aws_access_token = p.accessToken;
|
|
46
77
|
creds.default.aws_sso_authn = p.ssoAuthn;
|
|
47
|
-
if (p.refreshToken
|
|
48
|
-
p.clientId &&
|
|
49
|
-
p.clientSecret &&
|
|
50
|
-
p.clientSecretExpiresAt &&
|
|
51
|
-
p.accessTokenExpiresAt) {
|
|
78
|
+
if (p.refreshToken) {
|
|
52
79
|
creds.default.aws_sso_refresh_token = p.refreshToken;
|
|
80
|
+
}
|
|
81
|
+
if (p.clientId) {
|
|
53
82
|
creds.default.aws_sso_client_id = p.clientId;
|
|
83
|
+
}
|
|
84
|
+
if (p.clientSecret) {
|
|
54
85
|
creds.default.aws_sso_client_secret = p.clientSecret;
|
|
86
|
+
}
|
|
87
|
+
if (p.clientSecretExpiresAt) {
|
|
55
88
|
creds.default.aws_sso_client_secret_expires_at =
|
|
56
89
|
p.clientSecretExpiresAt.toString();
|
|
90
|
+
}
|
|
91
|
+
if (p.accessTokenExpiresAt) {
|
|
57
92
|
creds.default.aws_sso_access_token_expires_at =
|
|
58
93
|
p.accessTokenExpiresAt.toString();
|
|
59
94
|
}
|
|
60
|
-
else {
|
|
61
|
-
delete creds.default.aws_sso_refresh_token;
|
|
62
|
-
delete creds.default.aws_sso_client_id;
|
|
63
|
-
delete creds.default.aws_sso_client_secret;
|
|
64
|
-
delete creds.default.aws_sso_client_secret_expires_at;
|
|
65
|
-
delete creds.default.aws_sso_access_token_expires_at;
|
|
66
|
-
}
|
|
67
95
|
}
|
|
68
96
|
const newcreds = (0, ini_1.stringify)(creds);
|
|
69
97
|
(0, log_1.info)(`saving updated default creds to .aws/credentials`);
|
|
@@ -73,6 +101,8 @@ const updateAwsCredentials = (p) => __awaiter(void 0, void 0, void 0, function*
|
|
|
73
101
|
fs_1.default.mkdirSync(credentialsDir, { recursive: true });
|
|
74
102
|
(0, log_1.info)(`created AWS credentials directory: ${credentialsDir}`);
|
|
75
103
|
}
|
|
76
|
-
|
|
104
|
+
(0, credentialsLock_1.writeFileAtomic)(credspath, newcreds);
|
|
77
105
|
});
|
|
106
|
+
exports.saveAwsCredentialsUnlocked = saveAwsCredentialsUnlocked;
|
|
107
|
+
const updateAwsCredentials = (p) => __awaiter(void 0, void 0, void 0, function* () { return (0, credentialsLock_1.withCredentialsLock)(() => (0, exports.saveAwsCredentialsUnlocked)(p)); });
|
|
78
108
|
exports.updateAwsCredentials = updateAwsCredentials;
|
package/dist/helpers/browser.js
CHANGED
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.allowAccessIfRequested = exports.dismissAllowAccessPrompt = exports.allowAccessSelectors = exports.goToPage = exports.launchBrowser = exports.closeBrowser = void 0;
|
|
13
13
|
exports.getMFA = getMFA;
|
|
14
14
|
const log_1 = require("ag-common/dist/common/helpers/log");
|
|
15
|
+
const sleep_1 = require("ag-common/dist/common/helpers/sleep");
|
|
15
16
|
const puppeteer_1 = require("puppeteer");
|
|
16
17
|
const __1 = require("..");
|
|
17
18
|
const config_1 = require("../config");
|
|
@@ -103,9 +104,7 @@ const clickSelectorIfPresent = (page_1, selector_1, ...args_1) => __awaiter(void
|
|
|
103
104
|
catch (_a) {
|
|
104
105
|
// Element may detach between query and click (SPA re-render).
|
|
105
106
|
// Fall back to an in-page click which re-queries the selector.
|
|
106
|
-
yield page
|
|
107
|
-
.$eval(selector, (bn) => bn.click())
|
|
108
|
-
.catch(() => undefined);
|
|
107
|
+
yield page.$eval(selector, (bn) => bn.click()).catch(() => undefined);
|
|
109
108
|
}
|
|
110
109
|
return true;
|
|
111
110
|
}
|
|
@@ -113,24 +112,23 @@ const clickSelectorIfPresent = (page_1, selector_1, ...args_1) => __awaiter(void
|
|
|
113
112
|
return false;
|
|
114
113
|
}
|
|
115
114
|
});
|
|
116
|
-
const clickAllowAccessByText = (page) =>
|
|
115
|
+
const clickAllowAccessByText = (page) => {
|
|
117
116
|
try {
|
|
118
|
-
|
|
117
|
+
const clicked = page.evaluate(() => {
|
|
119
118
|
const isVisible = (el) => {
|
|
120
119
|
const rect = el.getBoundingClientRect();
|
|
121
|
-
return
|
|
122
|
-
rect.height > 0 &&
|
|
123
|
-
getComputedStyle(el).visibility !== 'hidden');
|
|
120
|
+
return rect.width > 0 && rect.height > 0 && getComputedStyle(el).visibility !== 'hidden';
|
|
124
121
|
};
|
|
125
122
|
// Match button, link, or ARIA-button variants: AWS renders the
|
|
126
123
|
// "Allow access" consent action differently across releases
|
|
127
124
|
// (orange button vs link), so don't assume a <button>.
|
|
128
125
|
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], input[type="submit"], input[type="button"]')).filter((el) => isVisible(el));
|
|
129
126
|
const label = (el) => {
|
|
130
|
-
var _a
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
var _a;
|
|
128
|
+
const inputValue = el.value;
|
|
129
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- value is undefined on non-inputs
|
|
130
|
+
const text = typeof inputValue === 'string' ? inputValue : ((_a = el.textContent) !== null && _a !== void 0 ? _a : '');
|
|
131
|
+
return text.trim().toLowerCase();
|
|
134
132
|
};
|
|
135
133
|
const exact = candidates.find((el) => label(el) === 'allow access');
|
|
136
134
|
const target = exact !== null && exact !== void 0 ? exact : candidates.find((el) => label(el).includes('allow access'));
|
|
@@ -140,17 +138,17 @@ const clickAllowAccessByText = (page) => __awaiter(void 0, void 0, void 0, funct
|
|
|
140
138
|
}
|
|
141
139
|
return false;
|
|
142
140
|
});
|
|
141
|
+
return clicked;
|
|
143
142
|
}
|
|
144
143
|
catch (_a) {
|
|
145
|
-
return false;
|
|
144
|
+
return Promise.resolve(false);
|
|
146
145
|
}
|
|
147
|
-
}
|
|
146
|
+
};
|
|
148
147
|
const dismissAllowAccessPrompt = (page_1, ...args_1) => __awaiter(void 0, [page_1, ...args_1], void 0, function* (page, perSelectorTimeout = 1000) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
148
|
+
const clicked = yield Promise.all(exports.allowAccessSelectors.map((selector) => clickSelectorIfPresent(page, selector, perSelectorTimeout)));
|
|
149
|
+
if (clicked.some(Boolean)) {
|
|
150
|
+
(0, log_1.info)('click allow-access-button');
|
|
151
|
+
return true;
|
|
154
152
|
}
|
|
155
153
|
if (yield clickAllowAccessByText(page)) {
|
|
156
154
|
(0, log_1.info)('click allow-access-button (text match "Allow access")');
|
|
@@ -159,29 +157,40 @@ const dismissAllowAccessPrompt = (page_1, ...args_1) => __awaiter(void 0, [page_
|
|
|
159
157
|
return false;
|
|
160
158
|
});
|
|
161
159
|
exports.dismissAllowAccessPrompt = dismissAllowAccessPrompt;
|
|
160
|
+
const pollDelayMs = 500;
|
|
161
|
+
const sleepUntilDeadline = (deadline) => __awaiter(void 0, void 0, void 0, function* () {
|
|
162
|
+
const remaining = deadline - Date.now();
|
|
163
|
+
if (remaining <= 0) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
yield (0, sleep_1.sleep)(Math.min(pollDelayMs, remaining));
|
|
167
|
+
});
|
|
162
168
|
const allowAccessIfRequested = (page_1, ...args_1) => __awaiter(void 0, [page_1, ...args_1], void 0, function* (page, timeout = config_1.timeoutMs) {
|
|
163
169
|
const deadline = Date.now() + timeout;
|
|
164
170
|
// Poll so a slow-rendering consent dialog is not missed, while still
|
|
165
171
|
// returning quickly when the prompt appears (or never appears).
|
|
166
|
-
|
|
172
|
+
// oxlint-disable-next-line no-await-in-loop -- polling must stay sequential to match dialog timing
|
|
173
|
+
for (;;) {
|
|
174
|
+
// oxlint-disable-next-line no-await-in-loop -- each poll depends on the current page state
|
|
167
175
|
if (yield (0, exports.dismissAllowAccessPrompt)(page)) {
|
|
168
176
|
return true;
|
|
169
177
|
}
|
|
170
|
-
if (Date.now()
|
|
171
|
-
|
|
178
|
+
if (Date.now() >= deadline) {
|
|
179
|
+
return false;
|
|
172
180
|
}
|
|
173
|
-
|
|
181
|
+
// oxlint-disable-next-line no-await-in-loop -- delay must elapse before the next poll
|
|
182
|
+
yield sleepUntilDeadline(deadline);
|
|
174
183
|
}
|
|
175
|
-
return false;
|
|
176
184
|
});
|
|
177
185
|
exports.allowAccessIfRequested = allowAccessIfRequested;
|
|
178
186
|
const waitForSuccessDefensively = (page_1, ...args_1) => __awaiter(void 0, [page_1, ...args_1], void 0, function* (page, timeout = config_1.timeoutMs) {
|
|
179
|
-
var _a;
|
|
180
187
|
const deadline = Date.now() + timeout;
|
|
181
188
|
let lastError;
|
|
182
|
-
|
|
189
|
+
// oxlint-disable-next-line no-await-in-loop -- success and consent checks are order-dependent
|
|
190
|
+
for (;;) {
|
|
183
191
|
// Success already visible?
|
|
184
192
|
try {
|
|
193
|
+
// oxlint-disable-next-line no-await-in-loop -- must check before dismissing prompts
|
|
185
194
|
const success = yield page.waitForSelector('[data-analytics-alert="success"]', { timeout: 2000 });
|
|
186
195
|
if (success) {
|
|
187
196
|
return;
|
|
@@ -194,6 +203,7 @@ const waitForSuccessDefensively = (page_1, ...args_1) => __awaiter(void 0, [page
|
|
|
194
203
|
// This covers the "Allow <user> to access your data? / Allow access"
|
|
195
204
|
// dialog which otherwise blocks success indefinitely.
|
|
196
205
|
try {
|
|
206
|
+
// oxlint-disable-next-line no-await-in-loop -- prompt check follows the success check
|
|
197
207
|
if (yield (0, exports.dismissAllowAccessPrompt)(page)) {
|
|
198
208
|
(0, log_1.info)('dismissed allow-access prompt while waiting for success');
|
|
199
209
|
continue;
|
|
@@ -204,6 +214,7 @@ const waitForSuccessDefensively = (page_1, ...args_1) => __awaiter(void 0, [page
|
|
|
204
214
|
}
|
|
205
215
|
// Also re-check the other intermediate confirmations while polling.
|
|
206
216
|
try {
|
|
217
|
+
// oxlint-disable-next-line no-await-in-loop -- confirmation check follows the success check
|
|
207
218
|
if (yield clickSelectorIfPresent(page, '[data-analytics="accept-user-code"]', 1000)) {
|
|
208
219
|
(0, log_1.info)('click accept-user-code (while waiting for success)');
|
|
209
220
|
continue;
|
|
@@ -213,6 +224,7 @@ const waitForSuccessDefensively = (page_1, ...args_1) => __awaiter(void 0, [page
|
|
|
213
224
|
(0, log_1.debug)('error clicking accept-user-code while waiting:', e);
|
|
214
225
|
}
|
|
215
226
|
try {
|
|
227
|
+
// oxlint-disable-next-line no-await-in-loop -- auth request check follows the success check
|
|
216
228
|
if (yield clickSelectorIfPresent(page, '#cli_verification_btn', 1000)) {
|
|
217
229
|
(0, log_1.info)('clicking auth request button (while waiting for success)');
|
|
218
230
|
}
|
|
@@ -220,6 +232,11 @@ const waitForSuccessDefensively = (page_1, ...args_1) => __awaiter(void 0, [page
|
|
|
220
232
|
catch (e) {
|
|
221
233
|
(0, log_1.debug)('error clicking auth request button while waiting:', e);
|
|
222
234
|
}
|
|
235
|
+
if (Date.now() >= deadline) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
// oxlint-disable-next-line no-await-in-loop -- delay must elapse before the next poll
|
|
239
|
+
yield sleepUntilDeadline(deadline);
|
|
223
240
|
}
|
|
224
241
|
// Final wait to surface a clear TimeoutError if success never arrived.
|
|
225
242
|
try {
|
|
@@ -227,8 +244,11 @@ const waitForSuccessDefensively = (page_1, ...args_1) => __awaiter(void 0, [page
|
|
|
227
244
|
timeout: config_1.timeoutShortMs,
|
|
228
245
|
});
|
|
229
246
|
}
|
|
230
|
-
catch (
|
|
231
|
-
|
|
247
|
+
catch (_a) {
|
|
248
|
+
if (lastError instanceof Error) {
|
|
249
|
+
throw lastError;
|
|
250
|
+
}
|
|
251
|
+
throw new Error('Timed out waiting for success');
|
|
232
252
|
}
|
|
233
253
|
});
|
|
234
254
|
function getMFA(p) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `fn` while holding an exclusive, cross-process lock next to the
|
|
3
|
+
* credentials file. Credential updates must be serialised because a rotated
|
|
4
|
+
* OIDC refresh token is single-use: two overlapping runs that refresh at the
|
|
5
|
+
* same time leave a dead token on disk.
|
|
6
|
+
*/
|
|
7
|
+
export declare function withCredentialsLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
8
|
+
/**
|
|
9
|
+
* Writes via a temp file and an atomic rename so a crash can never leave a
|
|
10
|
+
* truncated credentials file (which would lose the refresh token).
|
|
11
|
+
*/
|
|
12
|
+
export declare const writeFileAtomic: (filePath: string, contents: string) => void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.writeFileAtomic = void 0;
|
|
16
|
+
exports.withCredentialsLock = withCredentialsLock;
|
|
17
|
+
/* eslint-disable no-await-in-loop */
|
|
18
|
+
const sleep_1 = require("ag-common/dist/common/helpers/sleep");
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const path_1 = require("path");
|
|
21
|
+
const config_1 = require("../config");
|
|
22
|
+
const getCredentialsFilepath_1 = require("./getCredentialsFilepath");
|
|
23
|
+
const getLockPath = () => `${(0, getCredentialsFilepath_1.getCredentialsFilepath)()}.lock`;
|
|
24
|
+
const ensureCredentialsDir = () => {
|
|
25
|
+
const dir = (0, path_1.dirname)((0, getCredentialsFilepath_1.getCredentialsFilepath)());
|
|
26
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
27
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const tryAcquire = () => {
|
|
31
|
+
try {
|
|
32
|
+
fs_1.default.mkdirSync(getLockPath());
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
if (e.code === 'EEXIST') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const isStale = () => {
|
|
43
|
+
try {
|
|
44
|
+
return (Date.now() - fs_1.default.statSync(getLockPath()).mtimeMs > config_1.credentialsLockStaleMs);
|
|
45
|
+
}
|
|
46
|
+
catch (_a) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const release = () => {
|
|
51
|
+
try {
|
|
52
|
+
fs_1.default.rmSync(getLockPath(), { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
catch (_a) {
|
|
55
|
+
//
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Runs `fn` while holding an exclusive, cross-process lock next to the
|
|
60
|
+
* credentials file. Credential updates must be serialised because a rotated
|
|
61
|
+
* OIDC refresh token is single-use: two overlapping runs that refresh at the
|
|
62
|
+
* same time leave a dead token on disk.
|
|
63
|
+
*/
|
|
64
|
+
function withCredentialsLock(fn) {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
ensureCredentialsDir();
|
|
67
|
+
const deadline = Date.now() + config_1.credentialsLockTimeoutMs;
|
|
68
|
+
while (!tryAcquire()) {
|
|
69
|
+
if (isStale()) {
|
|
70
|
+
release();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (Date.now() >= deadline) {
|
|
74
|
+
// Another process is holding the lock. Proceed best-effort rather than
|
|
75
|
+
// blocking the user forever; stale-lock handling bounds the risk.
|
|
76
|
+
return fn();
|
|
77
|
+
}
|
|
78
|
+
yield (0, sleep_1.sleep)(100);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const result = yield fn();
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
release();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Writes via a temp file and an atomic rename so a crash can never leave a
|
|
91
|
+
* truncated credentials file (which would lose the refresh token).
|
|
92
|
+
*/
|
|
93
|
+
const writeFileAtomic = (filePath, contents) => {
|
|
94
|
+
const dir = (0, path_1.dirname)(filePath);
|
|
95
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
96
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
const tmp = (0, path_1.join)(dir, `.${process.pid}.${Date.now()}.tmp`);
|
|
99
|
+
fs_1.default.writeFileSync(tmp, contents);
|
|
100
|
+
fs_1.default.renameSync(tmp, filePath);
|
|
101
|
+
};
|
|
102
|
+
exports.writeFileAtomic = writeFileAtomic;
|
|
@@ -3,4 +3,18 @@ export declare class InteractiveSignInRequiredError extends Error {
|
|
|
3
3
|
name: string;
|
|
4
4
|
}
|
|
5
5
|
export declare function requiresInteractiveSignIn(e: unknown): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* True when the access token is missing, has unknown expiry, or is within the
|
|
8
|
+
* proactive refresh window of expiring.
|
|
9
|
+
*/
|
|
10
|
+
export declare function needsRefresh(p: IAwsCreds): boolean;
|
|
6
11
|
export declare function refreshAccessToken(p: IAwsCreds): Promise<IAwsCreds>;
|
|
12
|
+
/**
|
|
13
|
+
* Refreshes the OIDC access token ahead of expiry so the rotating refresh
|
|
14
|
+
* token chain stays alive, then persists the result atomically under a lock.
|
|
15
|
+
*
|
|
16
|
+
* Terminal failures (rejected/expired refresh token) are rethrown so the
|
|
17
|
+
* caller can start an interactive sign-in. Transient failures keep the cached
|
|
18
|
+
* token and never wipe it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensureFreshCredentials(p: IAwsCreds): Promise<IAwsCreds>;
|
|
@@ -11,9 +11,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.InteractiveSignInRequiredError = void 0;
|
|
13
13
|
exports.requiresInteractiveSignIn = requiresInteractiveSignIn;
|
|
14
|
+
exports.needsRefresh = needsRefresh;
|
|
14
15
|
exports.refreshAccessToken = refreshAccessToken;
|
|
16
|
+
exports.ensureFreshCredentials = ensureFreshCredentials;
|
|
15
17
|
const client_sso_oidc_1 = require("@aws-sdk/client-sso-oidc");
|
|
18
|
+
const log_1 = require("ag-common/dist/common/helpers/log");
|
|
16
19
|
const config_1 = require("../config");
|
|
20
|
+
const awsconfig_1 = require("./awsconfig");
|
|
21
|
+
const credentialsLock_1 = require("./credentialsLock");
|
|
17
22
|
class InteractiveSignInRequiredError extends Error {
|
|
18
23
|
constructor() {
|
|
19
24
|
super(...arguments);
|
|
@@ -21,24 +26,41 @@ class InteractiveSignInRequiredError extends Error {
|
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
exports.InteractiveSignInRequiredError = InteractiveSignInRequiredError;
|
|
29
|
+
// Errors that mean the stored registration/token can never be reused.
|
|
30
|
+
const terminalErrorNames = [
|
|
31
|
+
'InteractiveSignInRequiredError',
|
|
32
|
+
'InvalidGrantException',
|
|
33
|
+
'InvalidClientException',
|
|
34
|
+
'UnauthorizedClientException',
|
|
35
|
+
];
|
|
24
36
|
function requiresInteractiveSignIn(e) {
|
|
25
|
-
return
|
|
26
|
-
[
|
|
27
|
-
'InteractiveSignInRequiredError',
|
|
28
|
-
'InvalidGrantException',
|
|
29
|
-
'InvalidClientException',
|
|
30
|
-
'UnauthorizedClientException',
|
|
31
|
-
].includes(e.name));
|
|
37
|
+
return e instanceof Error && terminalErrorNames.includes(e.name);
|
|
32
38
|
}
|
|
39
|
+
const hasRefreshMaterial = (p) => Boolean(p.refreshToken && p.clientId && p.clientSecret);
|
|
40
|
+
/**
|
|
41
|
+
* True when the access token is missing, has unknown expiry, or is within the
|
|
42
|
+
* proactive refresh window of expiring.
|
|
43
|
+
*/
|
|
44
|
+
function needsRefresh(p) {
|
|
45
|
+
if (!p.accessToken) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (!p.accessTokenExpiresAt) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return p.accessTokenExpiresAt - Date.now() <= config_1.refreshWindowMs;
|
|
52
|
+
}
|
|
53
|
+
const fingerprint = (t) => (t ? `${t.slice(0, 8)}...` : 'none');
|
|
33
54
|
function refreshAccessToken(p) {
|
|
34
55
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
-
if (!p
|
|
56
|
+
if (!hasRefreshMaterial(p)) {
|
|
36
57
|
throw new InteractiveSignInRequiredError('OIDC refresh details are missing');
|
|
37
58
|
}
|
|
38
59
|
if (p.clientSecretExpiresAt &&
|
|
39
60
|
p.clientSecretExpiresAt <= Math.floor(Date.now() / 1000)) {
|
|
40
61
|
throw new InteractiveSignInRequiredError('OIDC client registration has expired');
|
|
41
62
|
}
|
|
63
|
+
(0, log_1.info)(`refreshing OIDC token with refresh token ${fingerprint(p.refreshToken)}`);
|
|
42
64
|
const response = yield new client_sso_oidc_1.SSOOIDCClient({
|
|
43
65
|
region: config_1.identityCenterRegion,
|
|
44
66
|
}).send(new client_sso_oidc_1.CreateTokenCommand({
|
|
@@ -50,6 +72,45 @@ function refreshAccessToken(p) {
|
|
|
50
72
|
if (!response.accessToken || !response.expiresIn) {
|
|
51
73
|
throw new Error('OIDC refresh did not return an access token');
|
|
52
74
|
}
|
|
75
|
+
const rotated = Boolean(response.refreshToken && response.refreshToken !== p.refreshToken);
|
|
76
|
+
(0, log_1.info)(`OIDC refresh succeeded; refresh token ${rotated ? 'rotated' : 'reused'}`);
|
|
53
77
|
return Object.assign(Object.assign({}, p), { accessToken: response.accessToken, refreshToken: response.refreshToken || p.refreshToken, accessTokenExpiresAt: Date.now() + response.expiresIn * 1000 });
|
|
54
78
|
});
|
|
55
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Refreshes the OIDC access token ahead of expiry so the rotating refresh
|
|
82
|
+
* token chain stays alive, then persists the result atomically under a lock.
|
|
83
|
+
*
|
|
84
|
+
* Terminal failures (rejected/expired refresh token) are rethrown so the
|
|
85
|
+
* caller can start an interactive sign-in. Transient failures keep the cached
|
|
86
|
+
* token and never wipe it.
|
|
87
|
+
*/
|
|
88
|
+
function ensureFreshCredentials(p) {
|
|
89
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
if (!hasRefreshMaterial(p) || !needsRefresh(p)) {
|
|
91
|
+
return p;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const refreshed = yield (0, credentialsLock_1.withCredentialsLock)(() => __awaiter(this, void 0, void 0, function* () {
|
|
95
|
+
var _a;
|
|
96
|
+
// Another process may have refreshed while we waited for the lock.
|
|
97
|
+
const latest = (_a = (yield (0, awsconfig_1.readAwsCredentials)())) !== null && _a !== void 0 ? _a : p;
|
|
98
|
+
if (!hasRefreshMaterial(latest) || !needsRefresh(latest)) {
|
|
99
|
+
(0, log_1.info)('OIDC token already refreshed by another process; reusing it');
|
|
100
|
+
return latest;
|
|
101
|
+
}
|
|
102
|
+
const rotated = yield refreshAccessToken(latest);
|
|
103
|
+
yield (0, awsconfig_1.saveAwsCredentialsUnlocked)(rotated);
|
|
104
|
+
return rotated;
|
|
105
|
+
}));
|
|
106
|
+
return refreshed;
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
if (requiresInteractiveSignIn(e)) {
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
(0, log_1.warn)(`Unable to refresh OIDC token (${e instanceof Error ? e.name : 'unknown error'}); using cached token`);
|
|
113
|
+
return p;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
package/dist/helpers/sso.js
CHANGED
|
@@ -114,23 +114,24 @@ function getSamlAssertion(p, instance) {
|
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
const tryExistingCredentials = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
117
|
+
const loaded = yield (0, awsconfig_1.readAwsCredentials)();
|
|
118
|
+
if (!loaded) {
|
|
119
119
|
return undefined;
|
|
120
120
|
}
|
|
121
|
-
let credentials =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
let credentials = loaded;
|
|
122
|
+
// Refresh proactively, independent of the portal session: the portal token
|
|
123
|
+
// can still be valid while the OIDC token is stale, and letting the refresh
|
|
124
|
+
// token go unused is what eventually kills it.
|
|
125
|
+
try {
|
|
126
|
+
credentials = yield (0, oidcRefresh_1.ensureFreshCredentials)(credentials);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
if (!(0, oidcRefresh_1.requiresInteractiveSignIn)(e)) {
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
(0, log_1.warn)(`Unable to reuse the cached SSO session (${e instanceof Error ? e.name : 'unknown error'}); interactive sign-in required.`);
|
|
133
|
+
return emptyCredentials();
|
|
134
|
+
}
|
|
134
135
|
const refresh = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
136
|
credentials = yield (0, oidcRefresh_1.refreshAccessToken)(credentials);
|
|
136
137
|
yield (0, awsconfig_1.updateAwsCredentials)(credentials);
|
package/dist/index.js
CHANGED
|
@@ -74,6 +74,9 @@ function main(args) {
|
|
|
74
74
|
identityCenterRegion: config_1.identityCenterRegion,
|
|
75
75
|
ssoStartUrl: config_1.ssoStartUrl,
|
|
76
76
|
});
|
|
77
|
+
// Persist the fresh refresh registration before any AWS call can fail;
|
|
78
|
+
// otherwise a later error would throw away the only non-interactive token.
|
|
79
|
+
yield (0, awsconfig_1.updateAwsCredentials)(credentials);
|
|
77
80
|
(0, log_1.info)('get oidc creds');
|
|
78
81
|
credentials = yield (0, sso_1.getOIDCCredentialsFromAccessToken)(credentials);
|
|
79
82
|
}
|
|
@@ -86,8 +89,20 @@ function main(args) {
|
|
|
86
89
|
let debugRole = '';
|
|
87
90
|
if (instance.searchMetadata) {
|
|
88
91
|
(0, log_1.info)('account is native aws, directly connecting');
|
|
92
|
+
const accountId = instance.searchMetadata.AccountId;
|
|
89
93
|
try {
|
|
90
|
-
credentials = yield (0,
|
|
94
|
+
credentials = yield (0, oidcRefresh_1.ensureFreshCredentials)(credentials);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
if (!(0, oidcRefresh_1.requiresInteractiveSignIn)(e)) {
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
(0, log_1.warn)(`Unable to reuse the cached SSO session (${e instanceof Error ? e.name : 'unknown error'}); interactive sign-in required.`);
|
|
101
|
+
credentials = yield (0, oidc_1.requestMFA)({ identityCenterRegion: config_1.identityCenterRegion, ssoStartUrl: config_1.ssoStartUrl });
|
|
102
|
+
yield (0, awsconfig_1.updateAwsCredentials)(credentials);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
credentials = yield (0, sso_1.getOIDCCredentialsFromAccessToken)(credentials, accountId);
|
|
91
106
|
}
|
|
92
107
|
catch (e) {
|
|
93
108
|
if (!(e instanceof Error) || e.name !== 'UnauthorizedException') {
|
|
@@ -95,20 +110,20 @@ function main(args) {
|
|
|
95
110
|
}
|
|
96
111
|
try {
|
|
97
112
|
credentials = yield (0, oidcRefresh_1.refreshAccessToken)(credentials);
|
|
113
|
+
yield (0, awsconfig_1.updateAwsCredentials)(credentials);
|
|
98
114
|
}
|
|
99
|
-
catch (
|
|
100
|
-
if (!(0, oidcRefresh_1.requiresInteractiveSignIn)(
|
|
101
|
-
throw
|
|
115
|
+
catch (refreshError) {
|
|
116
|
+
if (!(0, oidcRefresh_1.requiresInteractiveSignIn)(refreshError)) {
|
|
117
|
+
throw refreshError;
|
|
102
118
|
}
|
|
103
|
-
(0, log_1.warn)(`Unable to reuse the cached SSO session (${
|
|
119
|
+
(0, log_1.warn)(`Unable to reuse the cached SSO session (${refreshError instanceof Error ? refreshError.name : 'unknown error'}); interactive sign-in required.`);
|
|
104
120
|
credentials = yield (0, oidc_1.requestMFA)({ identityCenterRegion: config_1.identityCenterRegion, ssoStartUrl: config_1.ssoStartUrl });
|
|
121
|
+
yield (0, awsconfig_1.updateAwsCredentials)(credentials);
|
|
105
122
|
}
|
|
106
|
-
|
|
107
|
-
yield (0, awsconfig_1.updateAwsCredentials)(credentials);
|
|
108
|
-
credentials = yield (0, sso_1.getOIDCCredentialsFromAccessToken)(credentials, instance.searchMetadata.AccountId);
|
|
123
|
+
credentials = yield (0, sso_1.getOIDCCredentialsFromAccessToken)(credentials, accountId);
|
|
109
124
|
}
|
|
110
125
|
credentials.region = config_1.targetRegion;
|
|
111
|
-
debugRole =
|
|
126
|
+
debugRole = accountId;
|
|
112
127
|
}
|
|
113
128
|
else {
|
|
114
129
|
(0, log_1.info)('account is external app, getting saml');
|
package/package.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ag-awsauth",
|
|
3
|
+
"version": "0.0.295",
|
|
4
|
+
"private": false,
|
|
3
5
|
"description": "auth to aws sso/iamv2 easily",
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"author": "andreigec@hotmail.com",
|
|
6
6
|
"license": "ISC",
|
|
7
|
-
"
|
|
8
|
-
"version": "0.0.293",
|
|
9
|
-
"preferGlobal": true,
|
|
7
|
+
"author": "andreigec@hotmail.com",
|
|
10
8
|
"bin": {
|
|
11
9
|
"ag-awsauth": "./bin/awsauth.js"
|
|
12
10
|
},
|
|
@@ -16,18 +14,21 @@
|
|
|
16
14
|
"README.md",
|
|
17
15
|
"LICENSE.md"
|
|
18
16
|
],
|
|
17
|
+
"main": "dist/index.js",
|
|
19
18
|
"dependencies": {
|
|
20
19
|
"@aws-sdk/client-sso": "3.972.0",
|
|
21
20
|
"@aws-sdk/client-sso-oidc": "3.972.0",
|
|
22
21
|
"@aws-sdk/client-sts": "3.972.0",
|
|
23
22
|
"@aws-sdk/shared-ini-file-loader": "3.374.0",
|
|
24
|
-
"ag-common": "0.0.
|
|
23
|
+
"ag-common": "0.0.911",
|
|
25
24
|
"cli-select": "1.1.2",
|
|
26
25
|
"dotenv": "17.2.3",
|
|
27
26
|
"envfile": "7.1.0",
|
|
28
|
-
"eslint-config-e7npm": "0.1.
|
|
27
|
+
"eslint-config-e7npm": "0.1.68",
|
|
29
28
|
"ini": "5.0.0",
|
|
30
29
|
"node-fetch": "3.3.2",
|
|
30
|
+
"oxfmt": "^0.65.0",
|
|
31
|
+
"oxlint": "^1.83.0",
|
|
31
32
|
"puppeteer": "24.35.0",
|
|
32
33
|
"readline-sync": "1.4.10",
|
|
33
34
|
"typescript": "5.9.3",
|
|
@@ -43,12 +44,14 @@
|
|
|
43
44
|
"resolutions": {
|
|
44
45
|
"ws": ">=8.17.1"
|
|
45
46
|
},
|
|
47
|
+
"preferGlobal": true,
|
|
46
48
|
"engines": {
|
|
47
49
|
"node": ">=20"
|
|
48
50
|
},
|
|
49
51
|
"scripts": {
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
+
"format": "e7-oxfmt --config .oxfmtrc.json --disable-nested-config .",
|
|
53
|
+
"lint": "e7-oxlint --config .oxlintrc.json --deny-warnings src && pnpm run typecheck",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
52
55
|
"test": "tsc && node --test test/*.test.js",
|
|
53
56
|
"start": "tsc && node bin/awsauth.js",
|
|
54
57
|
"build": "tsc"
|