Skip to content

Commit 9702e4b

Browse files
committed
feat: support sts token login
1 parent 389a983 commit 9702e4b

File tree

9 files changed

+218
-44
lines changed

9 files changed

+218
-44
lines changed

src/commands/login/index.ts

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,40 @@ import {
1515
} from '../../utils/fileUtils/index.js';
1616
import { validateCredentials } from '../../utils/validateCredentials.js';
1717

18+
/** Parse STS token string: "AccessKeyId,AccessKeySecret,SecurityToken" or JSON */
19+
function parseStsToken(
20+
raw: string
21+
): { accessKeyId: string; accessKeySecret: string; securityToken: string } | null {
22+
const s = raw.trim();
23+
if (!s) return null;
24+
if (s.startsWith('{')) {
25+
try {
26+
const o = JSON.parse(s) as Record<string, string>;
27+
const accessKeyId =
28+
o.AccessKeyId ?? o.accessKeyId;
29+
const accessKeySecret =
30+
o.AccessKeySecret ?? o.accessKeySecret;
31+
const securityToken =
32+
o.SecurityToken ?? o.securityToken;
33+
if (accessKeyId && accessKeySecret && securityToken) {
34+
return { accessKeyId, accessKeySecret, securityToken };
35+
}
36+
} catch {
37+
return null;
38+
}
39+
return null;
40+
}
41+
const parts = s.split(',').map((p) => p.trim());
42+
if (parts.length >= 3) {
43+
return {
44+
accessKeyId: parts[0],
45+
accessKeySecret: parts[1],
46+
securityToken: parts.slice(2).join(',').trim()
47+
};
48+
}
49+
return null;
50+
}
51+
1852
const login: CommandModule = {
1953
command: 'login',
2054
describe: `🔑 ${t('login_describe').d('Login to the server')}`,
@@ -29,6 +63,12 @@ const login: CommandModule = {
2963
alias: 'sk',
3064
describe: t('login_option_access_key_secret')?.d('AccessKey Secret'),
3165
type: 'string'
66+
})
67+
.option('sts-token', {
68+
describe: t('login_option_sts_token')?.d(
69+
'STS token: AccessKeyId,AccessKeySecret,SecurityToken (comma-separated, one-shot)'
70+
),
71+
type: 'string'
3272
});
3373
},
3474
handler: async (argv: ArgumentsCamelCase) => {
@@ -40,10 +80,15 @@ export default login;
4080

4181
export async function handleLogin(argv?: ArgumentsCamelCase): Promise<void> {
4282
generateDefaultConfig();
43-
if (process.env.ESA_ACCESS_KEY_ID && process.env.ESA_ACCESS_KEY_SECRET) {
83+
const envSecurityToken = process.env.ESA_SECURITY_TOKEN;
84+
if (
85+
process.env.ESA_ACCESS_KEY_ID &&
86+
process.env.ESA_ACCESS_KEY_SECRET
87+
) {
4488
const result = await validateCredentials(
4589
process.env.ESA_ACCESS_KEY_ID,
46-
process.env.ESA_ACCESS_KEY_SECRET
90+
process.env.ESA_ACCESS_KEY_SECRET,
91+
envSecurityToken
4792
);
4893
if (result.valid) {
4994
logger.log(
@@ -58,6 +103,38 @@ export async function handleLogin(argv?: ArgumentsCamelCase): Promise<void> {
58103
return;
59104
}
60105

106+
const stsTokenRaw = argv?.['sts-token'] as string | undefined;
107+
if (stsTokenRaw) {
108+
const parsed = parseStsToken(stsTokenRaw);
109+
if (!parsed) {
110+
logger.error(
111+
t('login_sts_token_format_invalid').d(
112+
'Invalid STS token format. Use: AccessKeyId,AccessKeySecret,SecurityToken'
113+
)
114+
);
115+
return;
116+
}
117+
const result = await validateCredentials(
118+
parsed.accessKeyId,
119+
parsed.accessKeySecret,
120+
parsed.securityToken
121+
);
122+
if (result.valid) {
123+
logger.success(t('login_success').d('Login success!'));
124+
updateCliConfigFile({
125+
auth: {
126+
accessKeyId: parsed.accessKeyId,
127+
accessKeySecret: parsed.accessKeySecret,
128+
securityToken: parsed.securityToken
129+
},
130+
...(result.endpoint ? { endpoint: result.endpoint } : {})
131+
});
132+
} else {
133+
logger.error(result.message || 'Login failed');
134+
}
135+
return;
136+
}
137+
61138
const accessKeyId = argv?.['access-key-id'] as string;
62139
const accessKeySecret = argv?.['access-key-secret'] as string;
63140
if (accessKeyId && accessKeySecret) {
@@ -88,7 +165,8 @@ export async function handleLogin(argv?: ArgumentsCamelCase): Promise<void> {
88165
) {
89166
const loginStatus = await validateCredentials(
90167
cliConfig.auth.accessKeyId,
91-
cliConfig.auth.accessKeySecret
168+
cliConfig.auth.accessKeySecret,
169+
cliConfig.auth.securityToken
92170
);
93171
if (loginStatus.valid) {
94172
logger.warn(t('login_already').d('You are already logged in.'));
@@ -121,6 +199,63 @@ export async function handleLogin(argv?: ArgumentsCamelCase): Promise<void> {
121199
}
122200

123201
export async function interactiveLogin(): Promise<void> {
202+
const loginMethod = (await clackSelect({
203+
message: t('login_method_select').d('Choose login method'),
204+
options: [
205+
{
206+
label: t('login_method_aksk').d('AK/SK (AccessKey ID + AccessKey Secret)'),
207+
value: 'aksk'
208+
},
209+
{
210+
label: t('login_method_sts').d(
211+
'STS Token (one-shot: AccessKeyId,AccessKeySecret,SecurityToken)'
212+
),
213+
value: 'sts'
214+
}
215+
]
216+
})) as 'aksk' | 'sts';
217+
218+
if (isCancel(loginMethod)) {
219+
return;
220+
}
221+
222+
if (loginMethod === 'sts') {
223+
const stsInput = (await clackText({
224+
message: t('login_sts_token_prompt').d(
225+
'Enter STS token (AccessKeyId,AccessKeySecret,SecurityToken):'
226+
)
227+
})) as string;
228+
if (isCancel(stsInput)) return;
229+
const parsed = parseStsToken(stsInput);
230+
if (!parsed) {
231+
logger.error(
232+
t('login_sts_token_format_invalid').d(
233+
'Invalid STS token format. Use: AccessKeyId,AccessKeySecret,SecurityToken'
234+
)
235+
);
236+
return;
237+
}
238+
const loginStatus = await validateCredentials(
239+
parsed.accessKeyId,
240+
parsed.accessKeySecret,
241+
parsed.securityToken
242+
);
243+
if (loginStatus.valid) {
244+
await updateCliConfigFile({
245+
auth: {
246+
accessKeyId: parsed.accessKeyId,
247+
accessKeySecret: parsed.accessKeySecret,
248+
securityToken: parsed.securityToken
249+
},
250+
...(loginStatus.endpoint ? { endpoint: loginStatus.endpoint } : {})
251+
});
252+
logger.success(t('login_success').d('Login success!'));
253+
} else {
254+
logger.error(loginStatus.message || 'Login failed');
255+
}
256+
return;
257+
}
258+
124259
const styledUrl = chalk.underline.blue(
125260
'https://ram.console.aliyun.com/manage/ak'
126261
);

src/commands/logout.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,17 @@ export async function handleLogout() {
2424
}
2525

2626
if (!cliConfig.auth) {
27-
cliConfig.auth = { accessKeyId: '', accessKeySecret: '' };
27+
cliConfig.auth = {
28+
accessKeyId: '',
29+
accessKeySecret: '',
30+
securityToken: ''
31+
};
2832
} else {
2933
cliConfig.auth.accessKeyId = '';
3034
cliConfig.auth.accessKeySecret = '';
35+
if ('securityToken' in cliConfig.auth) {
36+
cliConfig.auth.securityToken = '';
37+
}
3138
}
3239

3340
await updateCliConfigFile(cliConfig);

src/commands/utils.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,23 +104,28 @@ export async function checkIsLoginSuccess(): Promise<boolean> {
104104
process.env.ESA_ACCESS_KEY_ID || cliConfig?.auth?.accessKeyId;
105105
let accessKeySecret =
106106
process.env.ESA_ACCESS_KEY_SECRET || cliConfig?.auth?.accessKeySecret;
107+
const securityToken =
108+
process.env.ESA_SECURITY_TOKEN || cliConfig?.auth?.securityToken;
107109

108110
if (accessKeyId && accessKeySecret) {
109-
const result = await validateCredentials(accessKeyId, accessKeySecret);
111+
const result = await validateCredentials(
112+
accessKeyId,
113+
accessKeySecret,
114+
securityToken
115+
);
110116
const server = await ApiService.getInstance();
111117
if (result.valid) {
118+
const auth: { accessKeyId: string; accessKeySecret: string; securityToken?: string } = {
119+
accessKeyId,
120+
accessKeySecret
121+
};
122+
if (securityToken) auth.securityToken = securityToken;
112123
server.updateConfig({
113-
auth: {
114-
accessKeyId,
115-
accessKeySecret
116-
},
124+
auth,
117125
endpoint: result.endpoint
118126
});
119127
api.updateConfig({
120-
auth: {
121-
accessKeyId,
122-
accessKeySecret
123-
},
128+
auth,
124129
endpoint: result.endpoint
125130
});
126131
return true;

src/i18n/locales.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,30 @@
10751075
"en": "AccessKey Secret (SK)",
10761076
"zh_CN": "AccessKey Secret (SK)"
10771077
},
1078+
"login_option_sts_token": {
1079+
"en": "STS token: AccessKeyId,AccessKeySecret,SecurityToken (comma-separated, one-shot)",
1080+
"zh_CN": "STS 临时凭证:AccessKeyId,AccessKeySecret,SecurityToken(逗号分隔,一次性输入)"
1081+
},
1082+
"login_sts_token_prompt": {
1083+
"en": "Enter STS token (AccessKeyId,AccessKeySecret,SecurityToken):",
1084+
"zh_CN": "请输入 STS 凭证(AccessKeyId,AccessKeySecret,SecurityToken):"
1085+
},
1086+
"login_sts_token_format_invalid": {
1087+
"en": "Invalid STS token format. Use: AccessKeyId,AccessKeySecret,SecurityToken",
1088+
"zh_CN": "STS 凭证格式错误,请使用:AccessKeyId,AccessKeySecret,SecurityToken"
1089+
},
1090+
"login_method_select": {
1091+
"en": "Choose login method",
1092+
"zh_CN": "选择登录方式"
1093+
},
1094+
"login_method_aksk": {
1095+
"en": "AK/SK (AccessKey ID + AccessKey Secret)",
1096+
"zh_CN": "AK/SK(AccessKey ID + AccessKey Secret)"
1097+
},
1098+
"login_method_sts": {
1099+
"en": "STS Token (one-shot: AccessKeyId,AccessKeySecret,SecurityToken)",
1100+
"zh_CN": "STS 临时凭证(一次性输入:AccessKeyId,AccessKeySecret,SecurityToken)"
1101+
},
10781102
"login_option_skip_login": {
10791103
"en": "Skip login",
10801104
"zh_CN": "跳过登录"

src/libs/api.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ class Client {
2929
accessKeyId: config.auth?.accessKeyId,
3030
accessKeySecret: config.auth?.accessKeySecret,
3131
endpoint: config.endpoint,
32-
userAgent: CLI_USER_AGENT
32+
userAgent: CLI_USER_AGENT,
33+
...(config.auth?.securityToken
34+
? { securityToken: config.auth.securityToken }
35+
: {})
3336
});
3437
return new ESA.default(apiConfig as any);
3538
}

src/libs/apiService.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ export class ApiService {
5858
accessKeyId: cliConfig.auth?.accessKeyId,
5959
accessKeySecret: cliConfig.auth?.accessKeySecret,
6060
endpoint: cliConfig.endpoint,
61-
userAgent: CLI_USER_AGENT
61+
userAgent: CLI_USER_AGENT,
62+
...(cliConfig.auth?.securityToken
63+
? { securityToken: cliConfig.auth.securityToken }
64+
: {})
6265
});
6366

6467
this.client = new $OpenApi.default.default(apiConfig);
@@ -77,7 +80,10 @@ export class ApiService {
7780
accessKeyId: newConfig.auth?.accessKeyId,
7881
accessKeySecret: newConfig.auth?.accessKeySecret,
7982
endpoint: newConfig.endpoint,
80-
userAgent: CLI_USER_AGENT
83+
userAgent: CLI_USER_AGENT,
84+
...(newConfig.auth?.securityToken
85+
? { securityToken: newConfig.auth.securityToken }
86+
: {})
8187
});
8288
this.client = new $OpenApi.default.default(apiConfig);
8389
}

src/utils/fileUtils/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,8 @@ export const getApiConfig = () => {
305305
let defaultConfig = {
306306
auth: {
307307
accessKeyId: '',
308-
accessKeySecret: ''
308+
accessKeySecret: '',
309+
securityToken: undefined as string | undefined
309310
},
310311
endpoint: `esa.cn-hangzhou.aliyuncs.com`
311312
};
@@ -319,7 +320,8 @@ export const getApiConfig = () => {
319320
const config = {
320321
auth: {
321322
accessKeyId: combinedConfig.auth?.accessKeyId,
322-
accessKeySecret: combinedConfig.auth?.accessKeySecret
323+
accessKeySecret: combinedConfig.auth?.accessKeySecret,
324+
securityToken: combinedConfig.auth?.securityToken
323325
},
324326
endpoint: combinedConfig.endpoint
325327
};

src/utils/fileUtils/interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export interface AuthConfig {
22
accessKeyId: string;
33
accessKeySecret: string;
4+
/** STS 临时凭证中的 SecurityToken,使用 STS 登录时必填 */
5+
securityToken?: string;
46
}
57

68
export interface ProjectConfig {

0 commit comments

Comments
 (0)