ag-awsauth 0.0.294 → 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/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 +1 -1
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;
|
|
@@ -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');
|