Skip to content

Commit c720c1c

Browse files
authored
feat(credential-providers): add fromHttp credential provider (#5256)
1 parent 4398bfc commit c720c1c

22 files changed

+844
-1
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/node_modules/
2+
/build/
3+
/coverage/
4+
/docs/
5+
*.tsbuildinfo
6+
*.tgz
7+
*.log
8+
package-lock.json
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# @aws-sdk/credential-provider-http
2+
3+
[![NPM version](https://img.shields.io/npm/v/@aws-sdk/credential-provider-http/latest.svg)](https://www.npmjs.com/package/@aws-sdk/credential-provider-http)
4+
[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/credential-provider-http.svg)](https://www.npmjs.com/package/@aws-sdk/credential-provider-http)
5+
6+
> An internal transitively required package.
7+
8+
## Usage
9+
10+
See https://www.npmjs.com/package/@aws-sdk/credential-providers
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const base = require("../../jest.config.base.js");
2+
3+
module.exports = {
4+
...base,
5+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"name": "@aws-sdk/credential-provider-http",
3+
"version": "3.418.0",
4+
"description": "AWS credential provider for containers and HTTP sources",
5+
"main": "./dist-cjs/index.js",
6+
"module": "./dist-es/index.js",
7+
"scripts": {
8+
"build": "concurrently 'yarn:build:cjs' 'yarn:build:es' 'yarn:build:types'",
9+
"build:cjs": "tsc -p tsconfig.cjs.json",
10+
"build:es": "tsc -p tsconfig.es.json",
11+
"build:include:deps": "lerna run --scope $npm_package_name --include-dependencies build",
12+
"build:types": "tsc -p tsconfig.types.json",
13+
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
14+
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
15+
"test": "jest"
16+
},
17+
"keywords": [
18+
"aws",
19+
"credentials"
20+
],
21+
"author": {
22+
"name": "AWS SDK for JavaScript Team",
23+
"url": "https://aws.amazon.com/javascript/"
24+
},
25+
"license": "Apache-2.0",
26+
"dependencies": {
27+
"@aws-sdk/types": "*",
28+
"@smithy/protocol-http": "^3.0.5",
29+
"@smithy/property-provider": "^2.0.0",
30+
"@smithy/fetch-http-handler": "^2.1.5",
31+
"@smithy/node-http-handler": "^2.1.5",
32+
"@smithy/types": "^2.3.3",
33+
"tslib": "^2.5.0"
34+
},
35+
"devDependencies": {
36+
"@tsconfig/recommended": "1.0.1",
37+
"@types/node": "^14.14.31",
38+
"concurrently": "7.0.0",
39+
"downlevel-dts": "0.10.1",
40+
"rimraf": "3.0.2",
41+
"typedoc": "0.23.23",
42+
"typescript": "~4.9.5"
43+
},
44+
"types": "./dist-types/index.d.ts",
45+
"engines": {
46+
"node": ">=14.0.0"
47+
},
48+
"typesVersions": {
49+
"<4.0": {
50+
"dist-types/*": [
51+
"dist-types/ts3.4/*"
52+
]
53+
}
54+
},
55+
"files": [
56+
"dist-*/**"
57+
],
58+
"homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/credential-provider-http",
59+
"repository": {
60+
"type": "git",
61+
"url": "https://github.com/aws/aws-sdk-js-v3.git",
62+
"directory": "packages/credential-provider-http"
63+
},
64+
"typedoc": {
65+
"entryPoint": "src/index.ts"
66+
}
67+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CredentialsProviderError } from "@smithy/property-provider";
2+
3+
import { checkUrl } from "./checkUrl";
4+
5+
describe(checkUrl.name, () => {
6+
it("allows https", () => {
7+
expect(checkUrl(new URL("https://___.com"))).toBeUndefined();
8+
expect(() => checkUrl(new URL("http://___.com"))).toThrow(CredentialsProviderError);
9+
});
10+
11+
it("allows ECS container host", () => {
12+
expect(checkUrl(new URL("http://169.254.170.2/test"))).toBeUndefined();
13+
expect(() => checkUrl(new URL("http://169.254.170.3/test"))).toThrow(CredentialsProviderError);
14+
});
15+
16+
it("allows EKS container host", () => {
17+
expect(checkUrl(new URL("http://169.254.170.23/test"))).toBeUndefined();
18+
expect(() => checkUrl(new URL("http://169.254.170.24/test"))).toThrow(CredentialsProviderError);
19+
20+
expect(checkUrl(new URL("http://[fd00:ec2::23]/test"))).toBeUndefined();
21+
expect(() => checkUrl(new URL("http://[fd00:ec2::24]/test"))).toThrow(CredentialsProviderError);
22+
});
23+
24+
it("allows localhost", () => {
25+
expect(checkUrl(new URL("http://localhost/test"))).toBeUndefined();
26+
expect(checkUrl(new URL("http://127.0.0.0/test"))).toBeUndefined();
27+
expect(checkUrl(new URL("http://127.0.0.1/test"))).toBeUndefined();
28+
expect(checkUrl(new URL("http://127.255.255.255/test"))).toBeUndefined();
29+
expect(checkUrl(new URL("http://[::1]/test"))).toBeUndefined();
30+
expect(checkUrl(new URL("http://[0000:0000:0000:0000:0000:0000:0000:0001]/test"))).toBeUndefined();
31+
});
32+
33+
it("rejects other http", () => {
34+
expect(() => checkUrl(new URL("http://abcd.com"))).toThrow(CredentialsProviderError);
35+
});
36+
37+
describe("additional test cases", () => {
38+
it("rejects forbidden host in full URI", () => {
39+
expect(() => checkUrl(new URL("http://192.168.1.1/endpoint"))).toThrow(CredentialsProviderError);
40+
});
41+
it("rejects forbidden link-local host in full URI", () => {
42+
expect(() => checkUrl(new URL("http://169.254.170.3/endpoint"))).toThrow(CredentialsProviderError);
43+
});
44+
it("allows http loopback v4 URI", () => {
45+
expect(() => checkUrl(new URL("http://127.0.0.2/credentials"))).not.toThrow();
46+
});
47+
});
48+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { CredentialsProviderError } from "@smithy/property-provider";
2+
3+
/**
4+
* @internal
5+
* Anything starting with 127.
6+
*/
7+
const LOOPBACK_CIDR_IPv4 = "127.0.0.0/8";
8+
/**
9+
* @internal
10+
* A single IP equal to
11+
* 0000:0000:0000:0000:0000:0000:0000:0001
12+
*/
13+
const LOOPBACK_CIDR_IPv6 = "::1/128";
14+
/**
15+
* @internal
16+
*/
17+
const ECS_CONTAINER_HOST = "169.254.170.2";
18+
/**
19+
* @internal
20+
*/
21+
const EKS_CONTAINER_HOST_IPv4 = "169.254.170.23";
22+
/**
23+
* @internal
24+
*/
25+
const EKS_CONTAINER_HOST_IPv6 = "[fd00:ec2::23]";
26+
27+
/**
28+
* @internal
29+
*
30+
* @param url - to be validated.
31+
* @throws if not acceptable to this provider.
32+
*/
33+
export const checkUrl = (url: URL): void => {
34+
if (url.protocol === "https:") {
35+
// no additional requirements for HTTPS.
36+
return;
37+
}
38+
39+
if (
40+
url.hostname === ECS_CONTAINER_HOST ||
41+
url.hostname === EKS_CONTAINER_HOST_IPv4 ||
42+
url.hostname === EKS_CONTAINER_HOST_IPv6
43+
) {
44+
return;
45+
}
46+
47+
if (url.hostname.includes("[")) {
48+
// IPv6
49+
if (url.hostname === "[::1]" || url.hostname === "[0000:0000:0000:0000:0000:0000:0000:0001]") {
50+
return;
51+
}
52+
} else {
53+
// IPv4
54+
if (url.hostname === "localhost") {
55+
return;
56+
}
57+
const ipComponents = url.hostname.split(".");
58+
const inRange = (component: string): boolean => {
59+
const num = parseInt(component, 10);
60+
return 0 <= num && num <= 255;
61+
};
62+
if (
63+
ipComponents[0] === "127" &&
64+
inRange(ipComponents[1]) &&
65+
inRange(ipComponents[2]) &&
66+
inRange(ipComponents[3]) &&
67+
ipComponents.length === 4
68+
) {
69+
return;
70+
}
71+
}
72+
73+
throw new CredentialsProviderError(
74+
`URL not accepted. It must either be HTTPS or match one of the following:
75+
- loopback CIDR 127.0.0.0/8 or [::1/128]
76+
- ECS container host 169.254.170.2
77+
- EKS container host 169.254.170.23 or [fd00:ec2::23]`
78+
);
79+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { FetchHttpHandler } from "@smithy/fetch-http-handler";
2+
import { CredentialsProviderError } from "@smithy/property-provider";
3+
import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@smithy/types";
4+
5+
import { checkUrl } from "./checkUrl";
6+
import type { FromHttpOptions } from "./fromHttpTypes";
7+
import { createGetRequest, getCredentials } from "./requestHelpers";
8+
import { retryWrapper } from "./retry-wrapper";
9+
10+
/**
11+
* Creates a provider that gets credentials via HTTP request.
12+
*/
13+
export const fromHttp = (options: FromHttpOptions): AwsCredentialIdentityProvider => {
14+
let host: string;
15+
16+
const full = options.credentialsFullUri;
17+
18+
if (full) {
19+
host = full;
20+
} else {
21+
throw new CredentialsProviderError("No HTTP credential provider host provided.");
22+
}
23+
24+
// throws if invalid format.
25+
const url = new URL(host);
26+
27+
// throws if not to spec for provider.
28+
checkUrl(url);
29+
30+
const requestHandler = new FetchHttpHandler();
31+
32+
return retryWrapper(
33+
async (): Promise<AwsCredentialIdentity> => {
34+
const request = createGetRequest(url);
35+
if (options.authorizationToken) {
36+
request.headers.Authorization = options.authorizationToken;
37+
}
38+
const result = await requestHandler.handle(request);
39+
return getCredentials(result.response);
40+
},
41+
options.maxRetries ?? 3,
42+
options.timeout ?? 1000
43+
);
44+
};
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { HttpResponse } from "@smithy/protocol-http";
2+
import { Readable } from "stream";
3+
4+
import { fromHttp } from "./fromHttp";
5+
import * as helpers from "./requestHelpers";
6+
7+
const credentials = {
8+
accessKeyId: "ABC",
9+
secretAccessKey: "abcd",
10+
sessionToken: "abcde",
11+
expiration: new Date(),
12+
};
13+
14+
const mockToken = "abcd";
15+
16+
const mockResponse = {
17+
AccessKeyId: credentials.accessKeyId,
18+
SecretAccessKey: credentials.secretAccessKey,
19+
Token: credentials.sessionToken,
20+
AccountId: "123",
21+
Expiration: new Date(credentials.expiration).toISOString(), // rfc3339
22+
};
23+
24+
const mockHandle = jest.fn().mockResolvedValue({
25+
response: new HttpResponse({
26+
statusCode: 200,
27+
headers: {
28+
"Content-Type": "application/json",
29+
},
30+
body: Readable.from([""]),
31+
}),
32+
});
33+
34+
jest.mock("@smithy/node-http-handler", () => ({
35+
NodeHttpHandler: jest.fn().mockImplementation(() => ({
36+
destroy: () => {},
37+
handle: mockHandle,
38+
})),
39+
streamCollector: jest.fn(),
40+
}));
41+
42+
jest.spyOn(helpers, "getCredentials").mockReturnValue(Promise.resolve(credentials));
43+
44+
jest.mock("fs/promises", () => ({
45+
async readFile() {
46+
return mockToken;
47+
},
48+
}));
49+
50+
describe(fromHttp.name, () => {
51+
afterAll(() => {
52+
jest.resetAllMocks();
53+
});
54+
55+
it("uses the full uri", async () => {
56+
const provider = fromHttp({
57+
awsContainerCredentialsFullUri: "https://u1.aws",
58+
awsContainerCredentialsRelativeUri: "",
59+
});
60+
61+
await provider();
62+
63+
expect(mockHandle).toHaveBeenCalledWith(helpers.createGetRequest(new URL("https://u1.aws")));
64+
});
65+
66+
it("uses the relative uri", async () => {
67+
const provider = fromHttp({
68+
awsContainerCredentialsFullUri: "",
69+
awsContainerCredentialsRelativeUri: "/some-path",
70+
});
71+
72+
await provider();
73+
74+
expect(mockHandle).toHaveBeenCalledWith(helpers.createGetRequest(new URL("http://169.254.170.2/some-path")));
75+
});
76+
77+
it("can use the token", async () => {
78+
const provider = fromHttp({
79+
awsContainerCredentialsFullUri: "https://t1.aws",
80+
awsContainerAuthorizationToken: mockToken,
81+
});
82+
83+
const request = helpers.createGetRequest(new URL("https://t1.aws"));
84+
request.headers.Authorization = mockToken;
85+
86+
await provider();
87+
88+
expect(mockHandle).toHaveBeenCalledWith(request);
89+
});
90+
91+
it("can use the token file", async () => {
92+
const provider = fromHttp({
93+
awsContainerCredentialsFullUri: "https://t2.aws",
94+
awsContainerAuthorizationTokenFile: "some-file",
95+
});
96+
97+
const request = helpers.createGetRequest(new URL("https://t1.aws"));
98+
request.headers.Authorization = mockToken;
99+
100+
await provider();
101+
102+
expect(mockHandle).toHaveBeenCalledWith(request);
103+
});
104+
});

0 commit comments

Comments
 (0)