@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.
@@ -1,4 +1,4 @@
1
- export declare function buildFederationLoginUrl(input: {
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
- }): Promise<{
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>;
@@ -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 session = {
5
- sessionId: input.accessKeyId,
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
- const tokenHash = await sha256Hex(input.sessionToken);
14
- // Use region and tokenHash for cache key
15
- const cacheKey = `aws-signin:${input.region}:${tokenHash}`;
16
- const sessionJson = JSON.stringify(session);
17
- const encoded = encodeURIComponent(sessionJson);
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
- const rawToken = SigninToken;
48
- const baseLogin = `https://signin.aws.amazon.com/federation?Action=login` +
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=${rawToken}`;
51
- const intent = input.intent ?? "console";
52
- let destinationUrl;
62
+ `&SigninToken=${signinToken}` +
63
+ `&Destination=${encodeURIComponent(destination)}`);
64
+ }
65
+ function getDestinationUrl(region, intent) {
53
66
  switch (intent) {
54
67
  case "billing":
55
- destinationUrl =
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
- destinationUrl =
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
- destinationUrl =
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
- default:
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
- async function buildSignedHeaders() {
96
- const { signingKey } = await getSigningMaterial({
97
- certBase64: normalizedCert,
98
- privateKeyPkcs8Base64,
99
- });
100
- const host = `${SERVICE}.${region}.amazonaws.com`;
101
- const iso = new Date().toISOString();
102
- const amzDate = isoToAmzDate(iso);
103
- const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
104
- const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
105
- const payloadHash = await sha256Hex(body);
106
- const baseHeaders = {
107
- "content-type": "application/json",
108
- "host": host,
109
- "x-amz-date": amzDate,
110
- "x-amz-x509": normalizedCert,
111
- };
112
- const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
113
- const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
114
- const canonicalRequest = buildCanonicalRequest({
115
- method: "POST",
116
- canonicalUri: PATH,
117
- query: "",
118
- canonicalHeaders,
119
- signedHeaders,
120
- payloadHash,
121
- });
122
- const canonicalRequestHash = await sha256Hex(canonicalRequest);
123
- const stringToSign = buildStringToSign({
124
- algorithm: ALGORITHM,
125
- amzDate,
126
- credentialScope,
127
- canonicalRequestHash,
128
- });
129
- const signatureHex = await signStringToSign(stringToSign, signingKey);
130
- const headers = new Headers({
131
- "Content-Type": "application/json",
132
- "X-Amz-Date": amzDate,
133
- "X-Amz-X509": normalizedCert,
134
- "Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
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 - issuedAt) / 1000);
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(issuedAt + edgeCacheTtlSec * 1000).toISOString();
170
+ const edgeCacheExpiry = new Date(receivedAt + edgeCacheTtlSec * 1000).toISOString();
179
171
  return wrapResult(value, edgeCacheExpiry);
180
172
  }
181
173
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizamodo/aws-sts-core",
3
- "version": "0.4.33",
3
+ "version": "0.4.34",
4
4
  "description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",