ag-awsauth 0.0.290 → 0.0.292

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/README.md CHANGED
@@ -2,8 +2,45 @@
2
2
 
3
3
  ## Usage
4
4
 
5
- npm i -g awsauth
5
+ npm i -g ag-awsauth
6
6
 
7
7
  ag-awsauth -c
8
8
 
9
- ag-awsauth
9
+ ag-awsauth
10
+
11
+ ## Session duration
12
+
13
+ - SAML role sessions request 8 hours. The IAM role maximum session duration and
14
+ the SAML assertion can shorten this.
15
+ - Native IAM Identity Center role sessions use the duration configured on the
16
+ selected permission set. Set that permission set to 8 hours in IAM Identity
17
+ Center; the API does not accept a per-request duration.
18
+ - OIDC access tokens are refreshed automatically after the next interactive
19
+ sign-in. The refresh token and client registration are stored with the other
20
+ session details in the AWS credentials file.
21
+ - The AWS access portal has a separate user interactive session duration. Its
22
+ default is 8 hours, but an administrator can change it under IAM Identity
23
+ Center **Settings > Authentication > Session duration**. An external IdP or
24
+ Active Directory policy can impose a shorter duration.
25
+
26
+ ## Repeated sign-in prompts
27
+
28
+ Expired AWS role credentials do not necessarily mean the SSO session has
29
+ expired. A valid portal session can renew SAML role credentials directly;
30
+ native AWS accounts use the OIDC access token, refreshing it when AWS rejects
31
+ it. Rotated refresh tokens are saved immediately, even if a later request fails.
32
+
33
+ When AWS rejects the refresh token (`InvalidGrantException`), another sign-in
34
+ is required. An unexpired client registration alone does not make a refresh
35
+ token valid. The warning before the MFA prompt reports the reason. Portal
36
+ outages and network failures are reported as errors instead of requesting login.
37
+
38
+ Changes in a source checkout do not update a separately installed global
39
+ `ag-awsauth`. On PowerShell, `(Get-Command ag-awsauth).Source` shows which
40
+ installation the terminal runs.
41
+
42
+ ## Tests
43
+
44
+ Run `npm test`. Returning-user regressions run the real application entry point
45
+ in separate processes with temporary AWS files, simulated external AWS APIs,
46
+ and a prompt trap. They never contact AWS or use real credentials.
package/dist/config.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export declare const logPath = "log.txt";
2
2
  export declare const stsDurationSeconds: number;
3
- export declare const nativeStsDurationSeconds: number;
4
3
  export declare let basePath: string;
5
4
  export declare const setBasePath: (bp: string) => void;
6
5
  export declare let identityCenterRegion: string;
package/dist/config.js CHANGED
@@ -3,13 +3,12 @@ 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.nativeStsDurationSeconds = exports.stsDurationSeconds = exports.logPath = void 0;
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;
7
7
  const envfile_1 = require("envfile");
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  exports.logPath = 'log.txt';
11
- exports.stsDurationSeconds = 60 * 60 * 4; //4h
12
- exports.nativeStsDurationSeconds = 60 * 60 * 1; //1h
11
+ exports.stsDurationSeconds = 60 * 60 * 8;
13
12
  exports.basePath = '';
14
13
  const setBasePath = (bp) => {
15
14
  exports.basePath = bp;
@@ -44,6 +44,26 @@ const updateAwsCredentials = (p) => __awaiter(void 0, void 0, void 0, function*
44
44
  creds.default.aws_session_token = p.sessionToken;
45
45
  creds.default.aws_access_token = p.accessToken;
46
46
  creds.default.aws_sso_authn = p.ssoAuthn;
47
+ if (p.refreshToken &&
48
+ p.clientId &&
49
+ p.clientSecret &&
50
+ p.clientSecretExpiresAt &&
51
+ p.accessTokenExpiresAt) {
52
+ creds.default.aws_sso_refresh_token = p.refreshToken;
53
+ creds.default.aws_sso_client_id = p.clientId;
54
+ creds.default.aws_sso_client_secret = p.clientSecret;
55
+ creds.default.aws_sso_client_secret_expires_at =
56
+ p.clientSecretExpiresAt.toString();
57
+ creds.default.aws_sso_access_token_expires_at =
58
+ p.accessTokenExpiresAt.toString();
59
+ }
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
+ }
47
67
  }
48
68
  const newcreds = (0, ini_1.stringify)(creds);
49
69
  (0, log_1.info)(`saving updated default creds to .aws/credentials`);
@@ -2,6 +2,7 @@ import type { Page } from 'puppeteer';
2
2
  export declare const closeBrowser: () => Promise<void>;
3
3
  export declare const launchBrowser: () => Promise<void>;
4
4
  export declare const goToPage: (url: string) => Promise<Page>;
5
+ export declare const allowAccessIfRequested: (page: Page) => Promise<void>;
5
6
  export declare function getMFA(p: {
6
7
  verificationUriComplete: string;
7
8
  creds: {
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.goToPage = exports.launchBrowser = exports.closeBrowser = void 0;
12
+ exports.allowAccessIfRequested = 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
15
  const puppeteer_1 = require("puppeteer");
@@ -83,6 +83,15 @@ const goToPage = (url) => __awaiter(void 0, void 0, void 0, function* () {
83
83
  }
84
84
  });
85
85
  exports.goToPage = goToPage;
86
+ const allowAccessIfRequested = (page) => __awaiter(void 0, void 0, void 0, function* () {
87
+ const allowAccess = yield page.waitForSelector('[data-testid="allow-access-button"]', { timeout: config_1.timeoutMs, visible: true });
88
+ if (!allowAccess) {
89
+ throw new Error('allow access button not found');
90
+ }
91
+ (0, log_1.info)('click allow-access-button');
92
+ yield allowAccess.click();
93
+ });
94
+ exports.allowAccessIfRequested = allowAccessIfRequested;
86
95
  function getMFA(p) {
87
96
  return __awaiter(this, void 0, void 0, function* () {
88
97
  var _a;
@@ -208,33 +217,28 @@ function getMFA(p) {
208
217
  catch (e) {
209
218
  (0, log_1.debug)('error accepting user code:', e);
210
219
  }
220
+ let accessStateFound = false;
211
221
  try {
212
222
  (0, log_1.info)('allow-access-button');
213
- const messageDiv = yield page.waitForSelector('[data-testid="allow-access-button"]', {
214
- timeout: config_1.timeoutMedMs,
215
- });
216
- if (messageDiv) {
217
- (0, log_1.info)('click allow-access-button');
218
- yield messageDiv.click();
219
- }
220
- else {
221
- throw new Error('access prompt not found');
222
- }
223
+ yield (0, exports.allowAccessIfRequested)(page);
224
+ accessStateFound = true;
223
225
  }
224
226
  catch (e) {
225
227
  (0, log_1.debug)('error clicking allow-access-button:', e);
226
228
  }
227
- try {
228
- const verif = yield page.waitForSelector('#cli_verification_btn', {
229
- timeout: config_1.timeoutShortMs,
230
- });
231
- if (verif) {
232
- (0, log_1.info)('clicking auth request button');
233
- yield verif.click();
229
+ if (!accessStateFound) {
230
+ try {
231
+ const verif = yield page.waitForSelector('#cli_verification_btn', {
232
+ timeout: config_1.timeoutShortMs,
233
+ });
234
+ if (verif) {
235
+ (0, log_1.info)('clicking auth request button');
236
+ yield verif.click();
237
+ }
238
+ }
239
+ catch (e) {
240
+ (0, log_1.debug)('error clicking auth request button:', e);
234
241
  }
235
- }
236
- catch (e) {
237
- (0, log_1.debug)('error clicking auth request button:', e);
238
242
  }
239
243
  //
240
244
  (0, log_1.info)('waiting for success');
@@ -1,4 +1,5 @@
1
1
  import type { IAwsCreds } from '../types';
2
+ export { refreshAccessToken } from './oidcRefresh';
2
3
  export declare function requestMFA(p: {
3
4
  identityCenterRegion: string;
4
5
  ssoStartUrl: string;
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.refreshAccessToken = void 0;
12
13
  exports.requestMFA = requestMFA;
13
14
  /* eslint-disable no-await-in-loop */
14
15
  const client_sso_oidc_1 = require("@aws-sdk/client-sso-oidc");
@@ -18,6 +19,9 @@ const config_1 = require("../config");
18
19
  const browser_1 = require("./browser");
19
20
  const input_1 = require("./input");
20
21
  const version_1 = require("./version");
22
+ var oidcRefresh_1 = require("./oidcRefresh");
23
+ Object.defineProperty(exports, "refreshAccessToken", { enumerable: true, get: function () { return oidcRefresh_1.refreshAccessToken; } });
24
+ const deviceCodeGrant = 'urn:ietf:params:oauth:grant-type:device_code';
21
25
  function requestMFA(p) {
22
26
  return __awaiter(this, void 0, void 0, function* () {
23
27
  const sso_oidc = new client_sso_oidc_1.SSOOIDCClient({ region: p.identityCenterRegion });
@@ -27,7 +31,12 @@ function requestMFA(p) {
27
31
  const rcc = yield sso_oidc.send(new client_sso_oidc_1.RegisterClientCommand({
28
32
  clientName: creds.username,
29
33
  clientType: 'public',
34
+ grantTypes: [deviceCodeGrant, 'refresh_token'],
35
+ scopes: ['sso:account:access'],
30
36
  }));
37
+ if (!rcc.clientId || !rcc.clientSecret || !rcc.clientSecretExpiresAt) {
38
+ throw new Error('OIDC client registration details are missing');
39
+ }
31
40
  const sda = yield sso_oidc.send(new client_sso_oidc_1.StartDeviceAuthorizationCommand({
32
41
  clientId: rcc.clientId,
33
42
  clientSecret: rcc.clientSecret,
@@ -54,12 +63,28 @@ function requestMFA(p) {
54
63
  clientSecret: rcc.clientSecret,
55
64
  code: sda.userCode,
56
65
  deviceCode: sda.deviceCode,
57
- grantType: 'urn:ietf:params:oauth:grant-type:device_code',
66
+ grantType: deviceCodeGrant,
58
67
  }));
59
68
  accessToken = ctc.accessToken;
60
69
  if (!accessToken) {
61
70
  throw new Error('no access token');
62
71
  }
72
+ if (!ctc.refreshToken || !ctc.expiresIn) {
73
+ throw new Error('OIDC refresh details are missing');
74
+ }
75
+ return {
76
+ accessToken,
77
+ ssoAuthn,
78
+ refreshToken: ctc.refreshToken,
79
+ clientId: rcc.clientId,
80
+ clientSecret: rcc.clientSecret,
81
+ clientSecretExpiresAt: rcc.clientSecretExpiresAt,
82
+ accessTokenExpiresAt: Date.now() + ctc.expiresIn * 1000,
83
+ region: config_1.identityCenterRegion,
84
+ accessKeyId: '',
85
+ secretAccessKey: '',
86
+ sessionToken: '',
87
+ };
63
88
  }
64
89
  catch (e) {
65
90
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -71,13 +96,6 @@ function requestMFA(p) {
71
96
  yield (0, sleep_1.sleep)(5000);
72
97
  }
73
98
  } while (!accessToken);
74
- return {
75
- accessToken,
76
- ssoAuthn,
77
- region: config_1.identityCenterRegion,
78
- accessKeyId: '',
79
- secretAccessKey: '',
80
- sessionToken: '',
81
- };
99
+ throw new Error('no access token');
82
100
  });
83
101
  }
@@ -0,0 +1,6 @@
1
+ import type { IAwsCreds } from '../types';
2
+ export declare class InteractiveSignInRequiredError extends Error {
3
+ name: string;
4
+ }
5
+ export declare function requiresInteractiveSignIn(e: unknown): boolean;
6
+ export declare function refreshAccessToken(p: IAwsCreds): Promise<IAwsCreds>;
@@ -0,0 +1,55 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.InteractiveSignInRequiredError = void 0;
13
+ exports.requiresInteractiveSignIn = requiresInteractiveSignIn;
14
+ exports.refreshAccessToken = refreshAccessToken;
15
+ const client_sso_oidc_1 = require("@aws-sdk/client-sso-oidc");
16
+ const config_1 = require("../config");
17
+ class InteractiveSignInRequiredError extends Error {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.name = 'InteractiveSignInRequiredError';
21
+ }
22
+ }
23
+ exports.InteractiveSignInRequiredError = InteractiveSignInRequiredError;
24
+ function requiresInteractiveSignIn(e) {
25
+ return (e instanceof Error &&
26
+ [
27
+ 'InteractiveSignInRequiredError',
28
+ 'InvalidGrantException',
29
+ 'InvalidClientException',
30
+ 'UnauthorizedClientException',
31
+ ].includes(e.name));
32
+ }
33
+ function refreshAccessToken(p) {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ if (!p.refreshToken || !p.clientId || !p.clientSecret) {
36
+ throw new InteractiveSignInRequiredError('OIDC refresh details are missing');
37
+ }
38
+ if (p.clientSecretExpiresAt &&
39
+ p.clientSecretExpiresAt <= Math.floor(Date.now() / 1000)) {
40
+ throw new InteractiveSignInRequiredError('OIDC client registration has expired');
41
+ }
42
+ const response = yield new client_sso_oidc_1.SSOOIDCClient({
43
+ region: config_1.identityCenterRegion,
44
+ }).send(new client_sso_oidc_1.CreateTokenCommand({
45
+ clientId: p.clientId,
46
+ clientSecret: p.clientSecret,
47
+ grantType: 'refresh_token',
48
+ refreshToken: p.refreshToken,
49
+ }));
50
+ if (!response.accessToken || !response.expiresIn) {
51
+ throw new Error('OIDC refresh did not return an access token');
52
+ }
53
+ return Object.assign(Object.assign({}, p), { accessToken: response.accessToken, refreshToken: response.refreshToken || p.refreshToken, accessTokenExpiresAt: Date.now() + response.expiresIn * 1000 });
54
+ });
55
+ }
@@ -6,13 +6,8 @@ export declare const getAssumedRole: (p: {
6
6
  accountId: string;
7
7
  roleName: string;
8
8
  }>;
9
- export declare const getOIDCCredentialsFromAccessToken: (p: {
10
- accessToken: string;
11
- ssoAuthn: string;
12
- }) => Promise<IAwsCreds>;
13
- export declare function appInstances(p: {
14
- ssoAuthn: string;
15
- }): Promise<IAppInstance[]>;
9
+ export declare const getOIDCCredentialsFromAccessToken: (p: IAwsCreds, accountId?: string) => Promise<IAwsCreds>;
10
+ export declare function appInstances(p: Pick<IAwsCreds, 'accessToken' | 'ssoAuthn'>): Promise<IAppInstance[]>;
16
11
  export declare function getSamlAssertion(p: IAwsCreds, instance: IAppInstance): Promise<{
17
12
  samlAssertion: string;
18
13
  providerArn: string;
@@ -15,13 +15,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.tryExistingCredentials = exports.getOIDCCredentialsFromAccessToken = exports.getAssumedRole = void 0;
16
16
  exports.appInstances = appInstances;
17
17
  exports.getSamlAssertion = getSamlAssertion;
18
+ /* eslint-disable no-await-in-loop */
18
19
  const client_sso_1 = require("@aws-sdk/client-sso");
19
20
  const log_1 = require("ag-common/dist/common/helpers/log");
20
21
  const string_1 = require("ag-common/dist/common/helpers/string");
21
22
  const node_fetch_1 = __importDefault(require("node-fetch"));
22
23
  const config_1 = require("../config");
23
24
  const awsconfig_1 = require("./awsconfig");
24
- const sts_1 = require("./sts");
25
+ const oidcRefresh_1 = require("./oidcRefresh");
26
+ const portalTokens = (p) => [...new Set([p.ssoAuthn, p.accessToken])].filter(Boolean);
27
+ function fetchPortalJson(url, p, isValid) {
28
+ return __awaiter(this, void 0, void 0, function* () {
29
+ // Each token is tried in order because portal and OIDC sessions can expire independently.
30
+ for (const token of portalTokens(p)) {
31
+ const response = yield (0, node_fetch_1.default)(url, {
32
+ headers: { 'x-amz-sso_bearer_token': token },
33
+ });
34
+ if (response.status === 401) {
35
+ continue;
36
+ }
37
+ if (response.status >= 400) {
38
+ throw new Error(`AWS portal request failed (HTTP ${response.status})`);
39
+ }
40
+ const body = (yield response.json());
41
+ if (!isValid(body)) {
42
+ throw new Error('AWS portal returned an unexpected response');
43
+ }
44
+ return body;
45
+ }
46
+ throw new oidcRefresh_1.InteractiveSignInRequiredError('Cached portal tokens were rejected');
47
+ });
48
+ }
25
49
  const getAssumedRole = (p) => __awaiter(void 0, void 0, void 0, function* () {
26
50
  var _a, _b, _c, _d;
27
51
  const sso = new client_sso_1.SSOClient({ region: config_1.identityCenterRegion });
@@ -51,9 +75,9 @@ const getAssumedRole = (p) => __awaiter(void 0, void 0, void 0, function* () {
51
75
  return role;
52
76
  });
53
77
  exports.getAssumedRole = getAssumedRole;
54
- const getOIDCCredentialsFromAccessToken = (p) => __awaiter(void 0, void 0, void 0, function* () {
78
+ const getOIDCCredentialsFromAccessToken = (p, accountId) => __awaiter(void 0, void 0, void 0, function* () {
55
79
  const sso = new client_sso_1.SSOClient({ region: config_1.identityCenterRegion });
56
- const role = yield (0, exports.getAssumedRole)({ accessToken: p.accessToken });
80
+ const role = yield (0, exports.getAssumedRole)({ accessToken: p.accessToken, accountId });
57
81
  const ssoResp = yield sso.send(new client_sso_1.GetRoleCredentialsCommand(Object.assign(Object.assign({}, role), { accessToken: p.accessToken })));
58
82
  const rc = ssoResp.roleCredentials;
59
83
  if (!(rc === null || rc === void 0 ? void 0 : rc.accessKeyId) ||
@@ -67,25 +91,19 @@ const getOIDCCredentialsFromAccessToken = (p) => __awaiter(void 0, void 0, void
67
91
  exports.getOIDCCredentialsFromAccessToken = getOIDCCredentialsFromAccessToken;
68
92
  function appInstances(p) {
69
93
  return __awaiter(this, void 0, void 0, function* () {
70
- const ai = (yield (yield (0, node_fetch_1.default)(`https://portal.sso.${config_1.identityCenterRegion}.amazonaws.com/instance/appinstances`, { headers: { 'x-amz-sso_bearer_token': p.ssoAuthn } })).json());
71
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
72
- if (!ai.result) {
73
- throw new Error('appinstance error' + JSON.stringify(ai, null, 2));
74
- }
94
+ const ai = yield fetchPortalJson(`https://portal.sso.${config_1.identityCenterRegion}.amazonaws.com/instance/appinstances`, p, (body) => Boolean(body.result));
75
95
  return ai.result.sort((a, b) => (a.name < b.name ? -1 : 1));
76
96
  });
77
97
  }
78
98
  function getSamlAssertion(p, instance) {
79
99
  return __awaiter(this, void 0, void 0, function* () {
80
100
  var _a;
81
- const det = (yield (yield (0, node_fetch_1.default)(`https://portal.sso.${config_1.identityCenterRegion}.amazonaws.com/instance/appinstance/${instance.id}/profiles`, { headers: { 'x-amz-sso_bearer_token': p.ssoAuthn } })).json());
101
+ const det = yield fetchPortalJson(`https://portal.sso.${config_1.identityCenterRegion}.amazonaws.com/instance/appinstance/${instance.id}/profiles`, p, (body) => { var _a; return Boolean((_a = body.result[0]) === null || _a === void 0 ? void 0 : _a.url); });
82
102
  const asserturl = (_a = det.result[0]) === null || _a === void 0 ? void 0 : _a.url;
83
103
  if (!asserturl) {
84
104
  throw new Error('assertion url cant be found');
85
105
  }
86
- const assertion = (yield (yield (0, node_fetch_1.default)(asserturl, {
87
- headers: { 'x-amz-sso_bearer_token': p.ssoAuthn },
88
- })).json());
106
+ const assertion = yield fetchPortalJson(asserturl, p, (body) => Boolean(body.encodedResponse));
89
107
  const decoded = (0, string_1.fromBase64)(assertion.encodedResponse);
90
108
  const res = new RegExp(/<saml2:AttributeValue xmlns:xsi="http:\/\/www.w3.org\/2001\/XMLSchema-instance" xsi:type="xsd:string">(arn.*?)</gim).exec(decoded);
91
109
  if (!(res === null || res === void 0 ? void 0 : res[1])) {
@@ -96,7 +114,6 @@ function getSamlAssertion(p, instance) {
96
114
  });
97
115
  }
98
116
  const tryExistingCredentials = () => __awaiter(void 0, void 0, void 0, function* () {
99
- var _a;
100
117
  const credraw = yield (0, awsconfig_1.getAwsCredentials)();
101
118
  if (!credraw.default.aws_access_token) {
102
119
  return undefined;
@@ -107,40 +124,46 @@ const tryExistingCredentials = () => __awaiter(void 0, void 0, void 0, function*
107
124
  sessionToken: credraw.default.aws_session_token,
108
125
  accessToken: credraw.default.aws_access_token,
109
126
  ssoAuthn: credraw.default.aws_sso_authn,
127
+ refreshToken: credraw.default.aws_sso_refresh_token,
128
+ clientId: credraw.default.aws_sso_client_id,
129
+ clientSecret: credraw.default.aws_sso_client_secret,
130
+ clientSecretExpiresAt: Number(credraw.default.aws_sso_client_secret_expires_at) || undefined,
131
+ accessTokenExpiresAt: Number(credraw.default.aws_sso_access_token_expires_at) || undefined,
110
132
  region: config_1.identityCenterRegion,
111
133
  };
112
- const v = yield (0, sts_1.validateCredentials)(credentials);
113
- if (v) {
114
- return credentials;
134
+ const refresh = () => __awaiter(void 0, void 0, void 0, function* () {
135
+ credentials = yield (0, oidcRefresh_1.refreshAccessToken)(credentials);
136
+ yield (0, awsconfig_1.updateAwsCredentials)(credentials);
137
+ });
138
+ try {
139
+ yield appInstances(credentials);
115
140
  }
116
- if (credraw.default.aws_access_token && credraw.default.aws_sso_authn) {
141
+ catch (e) {
142
+ if (!(0, oidcRefresh_1.requiresInteractiveSignIn)(e)) {
143
+ throw e;
144
+ }
117
145
  try {
118
- (0, log_1.info)('trying oidc refresh');
119
- credentials = yield (0, exports.getOIDCCredentialsFromAccessToken)({
120
- accessToken: credraw.default.aws_access_token,
121
- ssoAuthn: credraw.default.aws_sso_authn,
122
- });
123
- return credentials;
146
+ yield refresh();
147
+ yield appInstances(credentials);
124
148
  }
125
149
  catch (e) {
126
- //
127
- const em = e.toString();
128
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
129
- if ((_a = em.includes('is expired')) !== null && _a !== void 0 ? _a : em.includes('or invalid')) {
130
- (0, log_1.info)('access token or sso expired, need to wipe');
131
- }
132
- else {
133
- (0, log_1.info)(e);
150
+ if (!(0, oidcRefresh_1.requiresInteractiveSignIn)(e)) {
151
+ throw e;
134
152
  }
153
+ (0, log_1.warn)(`Unable to reuse the cached SSO session (${e instanceof Error ? e.name : 'unknown error'}); interactive sign-in required.`);
154
+ return emptyCredentials();
135
155
  }
136
156
  }
137
- return {
138
- accessToken: '',
139
- ssoAuthn: '',
140
- region: config_1.identityCenterRegion,
141
- accessKeyId: '',
142
- secretAccessKey: '',
143
- sessionToken: '',
144
- };
157
+ // Role credentials and portal sessions expire independently. Renew the role
158
+ // only after the user selects an application; SAML does not need OIDC.
159
+ return credentials;
145
160
  });
146
161
  exports.tryExistingCredentials = tryExistingCredentials;
162
+ const emptyCredentials = () => ({
163
+ accessToken: '',
164
+ ssoAuthn: '',
165
+ region: config_1.identityCenterRegion,
166
+ accessKeyId: '',
167
+ secretAccessKey: '',
168
+ sessionToken: '',
169
+ });
@@ -1,4 +1,4 @@
1
- import type { IAwsCreds, SearchMetadata } from '../types';
1
+ import type { IAwsCreds } from '../types';
2
2
  export declare function validateCredentials(credentials: IAwsCreds): Promise<{
3
3
  accountId: string;
4
4
  principalArn: string;
@@ -10,15 +10,3 @@ export declare function getApplicationCreds(p: {
10
10
  providerArn: string;
11
11
  roleArn: string;
12
12
  }): Promise<IAwsCreds>;
13
- export declare function directStsAssume(p: {
14
- credentials: IAwsCreds;
15
- targetRegion: string;
16
- metadata: SearchMetadata;
17
- }): Promise<{
18
- region: string;
19
- accessKeyId: string;
20
- secretAccessKey: string;
21
- sessionToken: string;
22
- accessToken: string;
23
- ssoAuthn: string;
24
- }>;
@@ -11,11 +11,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.validateCredentials = validateCredentials;
13
13
  exports.getApplicationCreds = getApplicationCreds;
14
- exports.directStsAssume = directStsAssume;
15
14
  const client_sts_1 = require("@aws-sdk/client-sts");
16
15
  const log_1 = require("ag-common/dist/common/helpers/log");
17
16
  const config_1 = require("../config");
18
- const sso_1 = require("./sso");
19
17
  function validateCredentials(credentials) {
20
18
  return __awaiter(this, void 0, void 0, function* () {
21
19
  var _a;
@@ -25,7 +23,6 @@ function validateCredentials(credentials) {
25
23
  });
26
24
  try {
27
25
  const stub = yield sts.getCallerIdentity({});
28
- yield (0, sso_1.appInstances)(credentials);
29
26
  if (((_a = stub.$metadata.httpStatusCode) !== null && _a !== void 0 ? _a : 500) < 400 &&
30
27
  stub.Account &&
31
28
  stub.Arn) {
@@ -68,30 +65,3 @@ function getApplicationCreds(p) {
68
65
  return Object.assign(Object.assign({}, p.originCreds), { region: p.targetRegion, accessKeyId: ret.Credentials.AccessKeyId, secretAccessKey: ret.Credentials.SecretAccessKey, sessionToken: ret.Credentials.SessionToken });
69
66
  });
70
67
  }
71
- function directStsAssume(p) {
72
- return __awaiter(this, void 0, void 0, function* () {
73
- var _a, _b;
74
- const role = yield (0, sso_1.getAssumedRole)({
75
- accessToken: p.credentials.accessToken,
76
- accountId: p.metadata.AccountId,
77
- });
78
- const sts = new client_sts_1.STS({
79
- credentials: p.credentials,
80
- region: p.targetRegion,
81
- });
82
- const ar = yield sts.assumeRole({
83
- RoleArn: `arn:aws:iam::${role.accountId}:role/${role.roleName}`,
84
- RoleSessionName: 'awsauth',
85
- DurationSeconds: config_1.nativeStsDurationSeconds,
86
- });
87
- if (((_a = ar.$metadata.httpStatusCode) !== null && _a !== void 0 ? _a : 500) >= 400) {
88
- throw new Error('assume role error' + JSON.stringify(ar, null, 2));
89
- }
90
- if (!((_b = ar.Credentials) === null || _b === void 0 ? void 0 : _b.AccessKeyId) ||
91
- !ar.Credentials.SecretAccessKey ||
92
- !ar.Credentials.SessionToken) {
93
- throw new Error('no creds');
94
- }
95
- return Object.assign(Object.assign({}, p.credentials), { region: p.targetRegion, accessKeyId: ar.Credentials.AccessKeyId, secretAccessKey: ar.Credentials.SecretAccessKey, sessionToken: ar.Credentials.SessionToken });
96
- });
97
- }
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ const config_1 = require("./config");
23
23
  const awsconfig_1 = require("./helpers/awsconfig");
24
24
  const input_1 = require("./helpers/input");
25
25
  const oidc_1 = require("./helpers/oidc");
26
+ const oidcRefresh_1 = require("./helpers/oidcRefresh");
26
27
  const sso_1 = require("./helpers/sso");
27
28
  const sts_1 = require("./helpers/sts");
28
29
  const version_1 = require("./helpers/version");
@@ -85,11 +86,28 @@ function main(args) {
85
86
  let debugRole = '';
86
87
  if (instance.searchMetadata) {
87
88
  (0, log_1.info)('account is native aws, directly connecting');
88
- credentials = yield (0, sts_1.directStsAssume)({
89
- credentials,
90
- targetRegion: config_1.targetRegion,
91
- metadata: instance.searchMetadata,
92
- });
89
+ try {
90
+ credentials = yield (0, sso_1.getOIDCCredentialsFromAccessToken)(credentials, instance.searchMetadata.AccountId);
91
+ }
92
+ catch (e) {
93
+ if (!(e instanceof Error) || e.name !== 'UnauthorizedException') {
94
+ throw e;
95
+ }
96
+ try {
97
+ credentials = yield (0, oidcRefresh_1.refreshAccessToken)(credentials);
98
+ }
99
+ catch (e) {
100
+ if (!(0, oidcRefresh_1.requiresInteractiveSignIn)(e)) {
101
+ throw e;
102
+ }
103
+ (0, log_1.warn)(`Unable to reuse the cached SSO session (${e instanceof Error ? e.name : 'unknown error'}); interactive sign-in required.`);
104
+ credentials = yield (0, oidc_1.requestMFA)({ identityCenterRegion: config_1.identityCenterRegion, ssoStartUrl: config_1.ssoStartUrl });
105
+ }
106
+ // Save rotated tokens before another AWS request can fail.
107
+ yield (0, awsconfig_1.updateAwsCredentials)(credentials);
108
+ credentials = yield (0, sso_1.getOIDCCredentialsFromAccessToken)(credentials, instance.searchMetadata.AccountId);
109
+ }
110
+ credentials.region = config_1.targetRegion;
93
111
  debugRole = instance.searchMetadata.AccountId;
94
112
  }
95
113
  else {
package/dist/types.d.ts CHANGED
@@ -5,6 +5,11 @@ export interface IAwsCreds {
5
5
  region: string;
6
6
  accessToken: string;
7
7
  ssoAuthn: string;
8
+ refreshToken?: string;
9
+ clientId?: string;
10
+ clientSecret?: string;
11
+ clientSecretExpiresAt?: number;
12
+ accessTokenExpiresAt?: number;
8
13
  }
9
14
  export interface IAwsCredsRaw {
10
15
  region: string;
@@ -13,6 +18,11 @@ export interface IAwsCredsRaw {
13
18
  aws_session_token: string;
14
19
  aws_access_token: string;
15
20
  aws_sso_authn: string;
21
+ aws_sso_refresh_token?: string;
22
+ aws_sso_client_id?: string;
23
+ aws_sso_client_secret?: string;
24
+ aws_sso_client_secret_expires_at?: string;
25
+ aws_sso_access_token_expires_at?: string;
16
26
  aws_application_id: string;
17
27
  }
18
28
  export interface IAppInstances {
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "author": "andreigec@hotmail.com",
6
6
  "license": "ISC",
7
7
  "private": false,
8
- "version": "0.0.290",
8
+ "version": "0.0.292",
9
9
  "preferGlobal": true,
10
10
  "bin": {
11
11
  "ag-awsauth": "./bin/awsauth.js"
@@ -49,6 +49,7 @@
49
49
  "scripts": {
50
50
  "lint": "eslint src",
51
51
  "format": "eslint src --fix",
52
+ "test": "tsc && node --test test/*.test.js",
52
53
  "start": "tsc && node bin/awsauth.js",
53
54
  "build": "tsc"
54
55
  }