@vizamodo/aws-sts-core 0.4.33 → 0.4.34
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/federation/login.d.ts +5 -3
- package/dist/federation/login.js +60 -66
- package/dist/sts/issue.js +43 -51
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface BuildFederationLoginUrlInput {
|
|
2
2
|
accessKeyId: string;
|
|
3
3
|
secretAccessKey: string;
|
|
4
4
|
sessionToken: string;
|
|
@@ -6,9 +6,11 @@ export declare function buildFederationLoginUrl(input: {
|
|
|
6
6
|
expiration: string;
|
|
7
7
|
intent?: "console" | "billing" | "dynamodb" | "ssm";
|
|
8
8
|
forceRefresh?: boolean;
|
|
9
|
-
}
|
|
9
|
+
}
|
|
10
|
+
export interface FederationLoginUrl {
|
|
10
11
|
ok: true;
|
|
11
12
|
loginUrl: string;
|
|
12
13
|
shortUrl: string;
|
|
13
14
|
region: string;
|
|
14
|
-
}
|
|
15
|
+
}
|
|
16
|
+
export declare function buildFederationLoginUrl(input: BuildFederationLoginUrlInput): Promise<FederationLoginUrl>;
|
package/dist/federation/login.js
CHANGED
|
@@ -1,82 +1,76 @@
|
|
|
1
1
|
import { sha256Hex } from "../crypto/sha256";
|
|
2
2
|
import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
3
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
4
|
+
/** AWS SigninToken TTL is ~15 minutes regardless of credential lifetime. */
|
|
5
|
+
const SIGNIN_TOKEN_TTL_SEC = 15 * 60;
|
|
6
|
+
// ── Main export ────────────────────────────────────────────────────────────
|
|
3
7
|
export async function buildFederationLoginUrl(input) {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
sessionKey: input.secretAccessKey,
|
|
7
|
-
sessionToken: input.sessionToken
|
|
8
|
-
};
|
|
9
|
-
const expiresAtMs = Date.parse(input.expiration);
|
|
8
|
+
const { accessKeyId, secretAccessKey, sessionToken, region, expiration, forceRefresh } = input;
|
|
9
|
+
const expiresAtMs = Date.parse(expiration);
|
|
10
10
|
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
11
11
|
throw new Error("[federation] invalid or expired credentials");
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const cacheKey = `aws-signin:${
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const SigninToken = await getCachedOrFetch(cacheKey, async () => {
|
|
19
|
-
const tokenResp = await fetch(`https://signin.aws.amazon.com/federation?Action=getSigninToken&Session=${encoded}`);
|
|
20
|
-
if (!tokenResp.ok) {
|
|
21
|
-
// best-effort: do not cache failures
|
|
22
|
-
throw new Error("[signin] failed to fetch SigninToken");
|
|
23
|
-
}
|
|
24
|
-
const json = await tokenResp.json();
|
|
25
|
-
const token = json?.SigninToken;
|
|
26
|
-
if (!token) {
|
|
27
|
-
// do not cache invalid response
|
|
28
|
-
throw new Error("[signin] empty SigninToken");
|
|
29
|
-
}
|
|
30
|
-
// SigninToken TTL ~15 minutes (AWS behavior)
|
|
31
|
-
const SIGNIN_TOKEN_TTL_SEC = 15 * 60;
|
|
32
|
-
// derive effective TTL = min(tokenTTL, credentialTTL)
|
|
33
|
-
const credRemainingSec = Math.floor((Date.parse(input.expiration) - Date.now()) / 1000);
|
|
34
|
-
const effectiveTtlSec = Math.min(SIGNIN_TOKEN_TTL_SEC, credRemainingSec);
|
|
35
|
-
// if credential too close to expiry → skip caching
|
|
36
|
-
if (effectiveTtlSec <= 0) {
|
|
37
|
-
return token;
|
|
38
|
-
}
|
|
39
|
-
const expiryIso = new Date(Date.now() + effectiveTtlSec * 1000).toISOString();
|
|
40
|
-
// restore primitive storage (cache layer now handles primitive safely)
|
|
41
|
-
return wrapResult(token, expiryIso);
|
|
42
|
-
}, { ttlSec: 60, ...(input.forceRefresh !== undefined ? { forceRefresh: input.forceRefresh } : {}) } // allow caller-controlled retry
|
|
43
|
-
);
|
|
44
|
-
if (!SigninToken) {
|
|
13
|
+
// Hash token for cache key — avoids exposing raw token in logs/storage.
|
|
14
|
+
const tokenHash = await sha256Hex(sessionToken);
|
|
15
|
+
const cacheKey = `aws-signin:${region}:${accessKeyId}:${tokenHash}`;
|
|
16
|
+
const signinToken = await getCachedOrFetch(cacheKey, () => fetchSigninToken({ accessKeyId, secretAccessKey, sessionToken, expiration }), { ttlSec: 60, forceRefresh });
|
|
17
|
+
if (!signinToken)
|
|
45
18
|
throw new Error("[federation] unable to obtain SigninToken");
|
|
19
|
+
const loginUrl = buildLoginUrl(signinToken, region, input.intent ?? "console");
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
loginUrl,
|
|
23
|
+
shortUrl: loginUrl.slice(0, 40) + "..." + loginUrl.slice(-60),
|
|
24
|
+
region,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function fetchSigninToken(input) {
|
|
28
|
+
const { accessKeyId, secretAccessKey, sessionToken, expiration } = input;
|
|
29
|
+
const session = JSON.stringify({
|
|
30
|
+
sessionId: accessKeyId,
|
|
31
|
+
sessionKey: secretAccessKey,
|
|
32
|
+
sessionToken,
|
|
33
|
+
});
|
|
34
|
+
let res;
|
|
35
|
+
try {
|
|
36
|
+
res = await fetch(`https://signin.aws.amazon.com/federation?Action=getSigninToken&Session=${encodeURIComponent(session)}`);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error("[signin] unreachable");
|
|
46
40
|
}
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
throw new Error("[signin] failed to fetch SigninToken");
|
|
43
|
+
const json = await res.json();
|
|
44
|
+
const token = json?.SigninToken;
|
|
45
|
+
if (!token)
|
|
46
|
+
throw new Error("[signin] empty SigninToken");
|
|
47
|
+
// Cache for min(SIGNIN_TOKEN_TTL, remaining credential lifetime).
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const credRemainingSec = Math.floor((Date.parse(expiration) - now) / 1000);
|
|
50
|
+
const cacheTtlSec = Math.min(SIGNIN_TOKEN_TTL_SEC, credRemainingSec);
|
|
51
|
+
if (cacheTtlSec <= 0)
|
|
52
|
+
return token; // credential too close to expiry — skip cache
|
|
53
|
+
const cacheExpiry = new Date(now + cacheTtlSec * 1000).toISOString();
|
|
54
|
+
return wrapResult(token, cacheExpiry);
|
|
55
|
+
}
|
|
56
|
+
// ── URL builder ────────────────────────────────────────────────────────────
|
|
57
|
+
function buildLoginUrl(signinToken, region, intent) {
|
|
58
|
+
const destination = getDestinationUrl(region, intent);
|
|
59
|
+
return (`https://signin.aws.amazon.com/federation` +
|
|
60
|
+
`?Action=login` +
|
|
49
61
|
`&Issuer=viza` +
|
|
50
|
-
`&SigninToken=${
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
`&SigninToken=${signinToken}` +
|
|
63
|
+
`&Destination=${encodeURIComponent(destination)}`);
|
|
64
|
+
}
|
|
65
|
+
function getDestinationUrl(region, intent) {
|
|
53
66
|
switch (intent) {
|
|
54
67
|
case "billing":
|
|
55
|
-
|
|
56
|
-
`https://us-east-1.console.aws.amazon.com/costmanagement/home?region=${input.region}#/home`;
|
|
57
|
-
break;
|
|
68
|
+
return `https://us-east-1.console.aws.amazon.com/costmanagement/home?region=${region}#/home`;
|
|
58
69
|
case "dynamodb":
|
|
59
|
-
|
|
60
|
-
`https://${input.region}.console.aws.amazon.com/dynamodbv2/home?region=${input.region}#dashboard`;
|
|
61
|
-
break;
|
|
70
|
+
return `https://${region}.console.aws.amazon.com/dynamodbv2/home?region=${region}#dashboard`;
|
|
62
71
|
case "ssm":
|
|
63
|
-
|
|
64
|
-
`https://${input.region}.console.aws.amazon.com/systems-manager/parameters/?region=${input.region}&tab=Table`;
|
|
65
|
-
break;
|
|
72
|
+
return `https://${region}.console.aws.amazon.com/systems-manager/parameters/?region=${region}&tab=Table`;
|
|
66
73
|
case "console":
|
|
67
|
-
|
|
68
|
-
destinationUrl =
|
|
69
|
-
`https://console.aws.amazon.com/?region=${input.region}`;
|
|
70
|
-
break;
|
|
74
|
+
return `https://console.aws.amazon.com/?region=${region}`;
|
|
71
75
|
}
|
|
72
|
-
const encodedDestination = encodeURIComponent(destinationUrl);
|
|
73
|
-
const loginUrl = `${baseLogin}&Destination=${encodedDestination}`;
|
|
74
|
-
const shortUrl = loginUrl.slice(0, 40) + "..." +
|
|
75
|
-
loginUrl.slice(-60);
|
|
76
|
-
return {
|
|
77
|
-
ok: true,
|
|
78
|
-
loginUrl,
|
|
79
|
-
shortUrl,
|
|
80
|
-
region: input.region
|
|
81
|
-
};
|
|
82
76
|
}
|
package/dist/sts/issue.js
CHANGED
|
@@ -92,55 +92,46 @@ export async function issueAwsCredentials(input) {
|
|
|
92
92
|
const certSerialDec = parseCertSerialDec(normalizedCert);
|
|
93
93
|
const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
94
94
|
return getCachedOrFetch(cacheKey, async () => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
return {
|
|
137
|
-
headers,
|
|
138
|
-
body,
|
|
139
|
-
host,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
const { headers: finalHeaders, body, host } = await buildSignedHeaders();
|
|
143
|
-
const issuedAt = Date.now();
|
|
95
|
+
const { signingKey } = await getSigningMaterial({
|
|
96
|
+
certBase64: normalizedCert,
|
|
97
|
+
privateKeyPkcs8Base64,
|
|
98
|
+
});
|
|
99
|
+
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
100
|
+
const iso = new Date().toISOString();
|
|
101
|
+
const amzDate = isoToAmzDate(iso);
|
|
102
|
+
const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
|
|
103
|
+
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
104
|
+
const payloadHash = await sha256Hex(body);
|
|
105
|
+
const baseHeaders = {
|
|
106
|
+
"content-type": "application/json",
|
|
107
|
+
"host": host,
|
|
108
|
+
"x-amz-date": amzDate,
|
|
109
|
+
"x-amz-x509": normalizedCert,
|
|
110
|
+
};
|
|
111
|
+
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
112
|
+
const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
113
|
+
const canonicalRequest = buildCanonicalRequest({
|
|
114
|
+
method: "POST",
|
|
115
|
+
canonicalUri: PATH,
|
|
116
|
+
query: "",
|
|
117
|
+
canonicalHeaders,
|
|
118
|
+
signedHeaders,
|
|
119
|
+
payloadHash,
|
|
120
|
+
});
|
|
121
|
+
const canonicalRequestHash = await sha256Hex(canonicalRequest);
|
|
122
|
+
const stringToSign = buildStringToSign({
|
|
123
|
+
algorithm: ALGORITHM,
|
|
124
|
+
amzDate,
|
|
125
|
+
credentialScope,
|
|
126
|
+
canonicalRequestHash,
|
|
127
|
+
});
|
|
128
|
+
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
129
|
+
const finalHeaders = new Headers({
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
"X-Amz-Date": amzDate,
|
|
132
|
+
"X-Amz-X509": normalizedCert,
|
|
133
|
+
"Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
|
|
134
|
+
});
|
|
144
135
|
let res;
|
|
145
136
|
try {
|
|
146
137
|
res = await fetch(`https://${host}${PATH}`, {
|
|
@@ -160,6 +151,7 @@ export async function issueAwsCredentials(input) {
|
|
|
160
151
|
}
|
|
161
152
|
const json = await res.json();
|
|
162
153
|
const creds = json?.credentialSet?.[0]?.credentials;
|
|
154
|
+
const receivedAt = Date.now();
|
|
163
155
|
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
164
156
|
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
165
157
|
throw new InternalError("aws_malformed_credentials");
|
|
@@ -172,10 +164,10 @@ export async function issueAwsCredentials(input) {
|
|
|
172
164
|
};
|
|
173
165
|
// derive TTL = 1/3 lifetime
|
|
174
166
|
const expiresAtMs = Date.parse(creds.expiration);
|
|
175
|
-
const credLifetimeSec = Math.floor((expiresAtMs -
|
|
167
|
+
const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
|
|
176
168
|
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
177
169
|
const edgeCacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
178
|
-
const edgeCacheExpiry = new Date(
|
|
170
|
+
const edgeCacheExpiry = new Date(receivedAt + edgeCacheTtlSec * 1000).toISOString();
|
|
179
171
|
return wrapResult(value, edgeCacheExpiry);
|
|
180
172
|
}
|
|
181
173
|
return value;
|