/
interceptor.ts
168 lines (146 loc) · 4.81 KB
/
interceptor.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { AxiosRequestConfig, AxiosRequestHeaders, Method } from "axios";
import { sign } from "aws4";
import buildUrl from "axios/lib/helpers/buildURL";
import combineURLs from "axios/lib/helpers/combineURLs";
import isAbsoluteURL from "axios/lib/helpers/isAbsoluteURL";
import { SimpleCredentialsProvider } from "./credentials/simpleCredentialsProvider";
import { AssumeRoleCredentialsProvider } from "./credentials/assumeRoleCredentialsProvider";
import { CredentialsProvider } from ".";
import { isCredentialsProvider } from "./credentials/isCredentialsProvider";
export interface InterceptorOptions {
/**
* Target service. Will use default aws4 behavior if not given.
*/
service?: string;
/**
* AWS region name. Will use default aws4 behavior if not given.
*/
region?: string;
/**
* Whether to sign query instead of adding Authorization header. Default to false.
*/
signQuery?: boolean;
/**
* ARN of the IAM Role to be assumed to get the credentials from.
* The credentials will be cached and automatically refreshed as needed.
* Will not be used if credentials are provided.
*/
assumeRoleArn?: string;
/**
* Number of seconds before the assumed Role expiration
* to invalidate the cache.
* Used only if assumeRoleArn is provided.
*/
assumedRoleExpirationMarginSec?: number;
}
export interface SigningOptions {
host?: string;
headers?: AxiosRequestHeaders;
path?: string;
body?: unknown;
region?: string;
service?: string;
signQuery?: boolean;
method?: string;
}
export interface Credentials {
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string;
}
export type InternalAxiosHeaders = Record<
Method | "common",
Record<string, string>
>;
/**
* Create an interceptor to add to the Axios request chain. This interceptor
* will sign requests with the AWSv4 signature.
*
* @example
* axios.interceptors.request.use(
* aws4Interceptor({ region: "eu-west-2", service: "execute-api" })
* );
*
* @param options The options to be used when signing a request
* @param credentials Credentials to be used to sign the request
*/
export const aws4Interceptor = (
options?: InterceptorOptions,
credentials?: Credentials | CredentialsProvider
): ((config: AxiosRequestConfig) => Promise<AxiosRequestConfig>) => {
let credentialsProvider: CredentialsProvider;
if (isCredentialsProvider(credentials)) {
credentialsProvider = credentials;
} else if (options?.assumeRoleArn && !credentials) {
credentialsProvider = new AssumeRoleCredentialsProvider({
roleArn: options.assumeRoleArn,
region: options.region,
expirationMarginSec: options.assumedRoleExpirationMarginSec,
});
} else {
credentialsProvider = new SimpleCredentialsProvider(credentials);
}
return async (config): Promise<AxiosRequestConfig> => {
if (!config.url) {
throw new Error(
"No URL present in request config, unable to sign request"
);
}
if (config.params) {
config.url = buildUrl(config.url, config.params, config.paramsSerializer);
delete config.params;
}
let url = config.url;
if (config.baseURL && !isAbsoluteURL(config.url)) {
url = combineURLs(config.baseURL, config.url);
}
const { host, pathname, search } = new URL(url);
const { data, headers, method } = config;
const transformRequest = getTransformer(config);
const transformedData = transformRequest(data, headers);
// Remove all the default Axios headers
const {
common,
delete: _delete, // 'delete' is a reserved word
get,
head,
post,
put,
patch,
...headersToSign
} = headers as any as InternalAxiosHeaders;
// Axios type definitions do not match the real shape of this object
const signingOptions: SigningOptions = {
method: method && method.toUpperCase(),
host,
path: pathname + search,
region: options?.region,
service: options?.service,
signQuery: options?.signQuery,
body: transformedData,
headers: headersToSign as any,
};
const resolvedCredentials = await credentialsProvider.getCredentials();
sign(signingOptions, resolvedCredentials);
config.headers = signingOptions.headers;
if (signingOptions.signQuery) {
const originalUrl = new URL(config.url);
const signedUrl = new URL(originalUrl.origin + signingOptions.path);
config.url = signedUrl.toString();
}
return config;
};
};
const getTransformer = (config: AxiosRequestConfig) => {
const { transformRequest } = config;
if (transformRequest) {
if (typeof transformRequest === "function") {
return transformRequest;
} else if (transformRequest.length) {
return transformRequest[0];
}
}
throw new Error(
"Could not get default transformRequest function from Axios defaults"
);
};