Skip to content

[typescript-axios] Add support for AWSv4 Signature #18215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/generators/typescript-axios.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|stringEnums|Generate string enums instead of objects for enum values.| |false|
|supportsES6|Generate code that conforms to ES6.| |false|
|useSingleRequestParameter|Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter.| |false|
|withAWSV4Signature|whether to include AWS v4 signature support| |false|
|withInterfaces|Setting this property to true will generate interfaces next to the default class implementations.| |false|
|withNodeImports|Setting this property to true adds imports for NodeJS| |false|
|withSeparateModelsAndApi|Put the model and api in separate folders and in separate classes. This requires in addition a value for 'apiPackage' and 'modelPackage'| |false|
Expand Down Expand Up @@ -265,7 +266,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|OAuth2_ClientCredentials|✗|OAS2,OAS3
|OAuth2_AuthorizationCode|✗|OAS2,OAS3
|SignatureAuth|✗|OAS3
|AWSV4Signature||ToolingExtension
|AWSV4Signature||ToolingExtension

### Wire Format Feature
| Name | Supported | Defined By |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ public class TypeScriptAxiosClientCodegen extends AbstractTypeScriptClientCodege
public static final String WITH_NODE_IMPORTS = "withNodeImports";
public static final String STRING_ENUMS = "stringEnums";
public static final String STRING_ENUMS_DESC = "Generate string enums instead of objects for enum values.";
public static final String WITH_AWSV4_SIGNATURE = "withAWSV4Signature";

protected String npmRepository = null;
protected Boolean stringEnums = false;
protected boolean withAWSV4Signature = false;

private String tsModelPackage = "";

Expand All @@ -53,7 +55,7 @@ public TypeScriptAxiosClientCodegen() {

modifyFeatureSet(features -> features
.includeDocumentationFeatures(DocumentationFeature.Readme)
.includeSecurityFeatures(SecurityFeature.BearerToken));
.includeSecurityFeatures(SecurityFeature.BearerToken, SecurityFeature.AWSV4Signature));

// clear import mapping (from default generator) as TS does not use it
// at the moment
Expand All @@ -73,6 +75,7 @@ public TypeScriptAxiosClientCodegen() {
this.cliOptions.add(new CliOption(USE_SINGLE_REQUEST_PARAMETER, "Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(WITH_NODE_IMPORTS, "Setting this property to true adds imports for NodeJS", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(STRING_ENUMS, STRING_ENUMS_DESC).defaultValue(String.valueOf(this.stringEnums)));
this.cliOptions.add(new CliOption(WITH_AWSV4_SIGNATURE, "whether to include AWS v4 signature support", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if instead of requiring a user to explicitly opt in, we derive it from the actual spec? If the spec uses aws4 security, we add the corresponding dependency to the generated code.

Is there a use case where a spec relies on AWS4, but a user want to opt out? Or vice versa - the spec doesn't use AWS4, but we still want to include it in the generate files?

Copy link
Author

@varqasim varqasim Mar 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally don't have such use case since my workflow would be creating two different SDKs for each scenario. Scenario A would be an SDK that is exclusive for inter-service communication (IAM authorized) and scenario B would be creating a separate SDK for a frontend client that would have the a bearer token (i.e Cognito User Pool) authentication/authorisation security on it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

K I'm going to be taking this effort over.

  • @amakhrov I think your suggestion is great. I will attempt to include this in my new PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amakhrov Detecting AWS Auth from the schema PR #21356

// Templates have no mapping between formatted property names and original base names so use only "original" and remove this option
removeOption(CodegenConstants.MODEL_PROPERTY_NAMING);
}
Expand Down Expand Up @@ -152,6 +155,11 @@ public void processOpts() {
addNpmPackageGeneration();
}

if (additionalProperties.containsKey(CodegenConstants.WITH_AWSV4_SIGNATURE_COMMENT)) {
this.setWithAWSV4Signature(Boolean.parseBoolean(additionalProperties.get(CodegenConstants.WITH_AWSV4_SIGNATURE_COMMENT).toString()));
}
additionalProperties.put(CodegenConstants.WITH_AWSV4_SIGNATURE_COMMENT, withAWSV4Signature);

}

@Override
Expand Down Expand Up @@ -266,6 +274,10 @@ public ModelsMap postProcessModels(ModelsMap objs) {
return objs;
}

public void setWithAWSV4Signature(boolean withAWSV4Signature) {
this.withAWSV4Signature = withAWSV4Signature;
}

/**
* Overriding toRegularExpression() to avoid escapeText() being called,
* as it would return a broken regular expression if any escaped character / metacharacter were present.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import FormData from 'form-data'
{{/withNodeImports}}
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction{{#withAWSV4Signature}}, setAWS4SignatureInterceptor{{/withAWSV4Signature}} } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import FormData from 'form-data'
{{/withNodeImports}}
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '{{apiRelativeToRoot}}common';
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction{{#withAWSV4Signature}}, setAWS4SignatureInterceptor{{/withAWSV4Signature}} } from '{{apiRelativeToRoot}}common';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError, operationServerMap } from '{{apiRelativeToRoot}}base';
{{#imports}}
Expand Down Expand Up @@ -71,6 +71,10 @@ export const {{classname}}AxiosParamCreator = function (configuration?: Configur
{{#authMethods}}
// authentication {{name}} required
{{#isApiKey}}
{{#withAWSV4Signature}}
// aws v4 signature authentication required
await setAWS4SignatureInterceptor(globalAxios, configuration)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this sets the interceptor to the shared axios instance - affecting all requests made with axios in the app

  • it's not limited to just endpoints configured for that in the spec
  • it adds multiple instances of the interceptor: every time a someOperationAxiosParamCreator function is called

I really think the security settings should be applied per endpoint rather than globally

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think the best way to achieve this? Would it be creating a "shared" axios instance just for the aws4 secured endpoints?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. It was my first thought, too. But I'm concerned it will create much more confusion down the line. Like, users might want to set some app-specific interceptors globally - but some endpoints won't be covered due to a standalone axios instance (please correct me if that's wrong - it's been some time since I actively worked with axios myself)

  2. Another option is not setting an interceptor per se (don't call axios.interceptor.request.use). An interceptor is essentially a function. You can call it directly inside your operation. This way you can do that granularly, conditionally - only when needed.

  3. A variation of the above is setting a custom global interceptor with a condition inside. Individual operations will add some optional flag to request config. The interceptor will analyze this flag and invoke the aws4-axios interceptor or not based on that.

I personally like option 2 best.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option 4:
If the primary use case is adding AWS4 to the whole spec (rather then to select individual operations in it), we don't even need to support it explicitly in the generator. A user can set a global inteceptor and call it a day :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option 4:
If the primary use case is adding AWS4 to the whole spec (rather then to select individual operations in it), we don't even need to support it explicitly in the generator. A user can set a global interceptor and call it a day :)

This is exactly my use case and to me it makes sense since an SDK that is built for inter-service communication only defines endpoints that are meant for such scenario and not any other. In my experience also an SDK uses a single type of authentication/authorization type and not multiple ones. Otherwise it's going to be a complex setup to handle. Shall we go with option 4?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, in this case perhaps you don't need any change in openapi-generator - you can set the global interceptor directly in your app

{{/withAWSV4Signature}}
{{#isKeyInHeader}}
await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration)
{{/isKeyInHeader}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

import type { Configuration } from "./configuration";
import type { RequestArgs } from "./base";
import type { AxiosInstance, AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosResponse } from "axios";
{{#withAWSV4Signature}}
import { aws4Interceptor } from "aws4-axios";
{{/withAWSV4Signature}}
import { RequiredError } from "./base";
{{#withNodeImports}}
import { URL, URLSearchParams } from 'url';
Expand Down Expand Up @@ -76,6 +79,25 @@ export const setOAuthToObject = async function (object: any, name: string, scope
}
}

{{#withAWSV4Signature}}
export const setAWS4SignatureInterceptor = async function (globalAxios: AxiosInstance, configuration?: Configuration) {
if (configuration && configuration.awsv4) {
const interceptor = aws4Interceptor({
options: {
region: configuration.awsv4?.options?.region ?? process.env.AWS_REGION ?? 'us-east-1',
service: configuration.awsv4?.options?.service ?? 'execute-api',
},
credentials: {
accessKeyId: configuration.awsv4?.credentials?.accessKeyId ?? process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: configuration.awsv4?.credentials?.secretAccessKey ?? process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: configuration.awsv4?.credentials?.sessionToken ?? process.env.AWS_SESSION_TOKEN
},
});
globalAxios.interceptors.request.use(interceptor);
}
}
{{/withAWSV4Signature}}

function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
if (parameter == null) return;
if (typeof parameter === "object") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
/* tslint:disable */
/* eslint-disable */
{{>licenseInfo}}

interface AWSv4Configuration {
options?: {
region?: string
service?: string
}
credentials?: {
accessKeyId?: string
secretAccessKey?: string,
sessionToken?: string
}
}

export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
awsv4?: AWSv4Configuration;
basePath?: string;
serverIndex?: number;
baseOptions?: any;
Expand Down Expand Up @@ -41,6 +53,17 @@ export class Configuration {
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* parameter for aws4 signature security
* @param {Object} AWS4Signature - AWS4 Signature security
* @param {string} options.region - aws region
* @param {string} options.service - name of the service.
* @param {string} credentials.accessKeyId - aws access key id
* @param {string} credentials.secretAccessKey - aws access key
* @param {string} credentials.sessionToken - aws session token
* @memberof Configuration
*/
awsv4?: AWSv4Configuration;
/**
* override base path
*
Expand Down Expand Up @@ -76,6 +99,7 @@ export class Configuration {
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.awsv4 = param.awsv4;
this.basePath = param.basePath;
this.serverIndex = param.serverIndex;
this.baseOptions = param.baseOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@
"prepare": "npm run build"
},
"dependencies": {
{{#withAWSV4Signature}}
"axios": "^1.6.1",
"aws4-axios": "^3.3.4"
{{/withAWSV4Signature}}
{{^withAWSV4Signature}}
"axios": "^1.6.1"
{{/withAWSV4Signature}}
},
"devDependencies": {
"@types/node": "^12.11.5",
Expand Down
3 changes: 2 additions & 1 deletion samples/client/echo_api/typescript-axios/build/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import type { Configuration } from "./configuration";
import type { RequestArgs } from "./base";
import type { AxiosInstance, AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosResponse } from "axios";
import { RequiredError } from "./base";

/**
Expand Down Expand Up @@ -84,6 +84,7 @@ export const setOAuthToObject = async function (object: any, name: string, scope
}
}


function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
if (parameter == null) return;
if (typeof parameter === "object") {
Expand Down
26 changes: 25 additions & 1 deletion samples/client/echo_api/typescript-axios/build/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* tslint:disable */
/* eslint-disable */
/**
* Echo Server API
* Echo Server API
Expand All @@ -13,11 +12,24 @@
*/


interface AWSv4Configuration {
options?: {
region?: string
service?: string
}
credentials?: {
accessKeyId?: string
secretAccessKey?: string,
sessionToken?: string
}
}

export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
awsv4?: AWSv4Configuration;
basePath?: string;
serverIndex?: number;
baseOptions?: any;
Expand Down Expand Up @@ -52,6 +64,17 @@ export class Configuration {
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* parameter for aws4 signature security
* @param {Object} AWS4Signature - AWS4 Signature security
* @param {string} options.region - aws region
* @param {string} options.service - name of the service.
* @param {string} credentials.accessKeyId - aws access key id
* @param {string} credentials.secretAccessKey - aws access key
* @param {string} credentials.sessionToken - aws session token
* @memberof Configuration
*/
awsv4?: AWSv4Configuration;
/**
* override base path
*
Expand Down Expand Up @@ -87,6 +110,7 @@ export class Configuration {
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.awsv4 = param.awsv4;
this.basePath = param.basePath;
this.serverIndex = param.serverIndex;
this.baseOptions = param.baseOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import type { Configuration } from "./configuration";
import type { RequestArgs } from "./base";
import type { AxiosInstance, AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosResponse } from "axios";
import { RequiredError } from "./base";

/**
Expand Down Expand Up @@ -84,6 +84,7 @@ export const setOAuthToObject = async function (object: any, name: string, scope
}
}


function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
if (parameter == null) return;
if (typeof parameter === "object") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* tslint:disable */
/* eslint-disable */
/**
* Example
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
Expand All @@ -13,11 +12,24 @@
*/


interface AWSv4Configuration {
options?: {
region?: string
service?: string
}
credentials?: {
accessKeyId?: string
secretAccessKey?: string,
sessionToken?: string
}
}

export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
awsv4?: AWSv4Configuration;
basePath?: string;
serverIndex?: number;
baseOptions?: any;
Expand Down Expand Up @@ -52,6 +64,17 @@ export class Configuration {
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* parameter for aws4 signature security
* @param {Object} AWS4Signature - AWS4 Signature security
* @param {string} options.region - aws region
* @param {string} options.service - name of the service.
* @param {string} credentials.accessKeyId - aws access key id
* @param {string} credentials.secretAccessKey - aws access key
* @param {string} credentials.sessionToken - aws session token
* @memberof Configuration
*/
awsv4?: AWSv4Configuration;
/**
* override base path
*
Expand Down Expand Up @@ -87,6 +110,7 @@ export class Configuration {
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.awsv4 = param.awsv4;
this.basePath = param.basePath;
this.serverIndex = param.serverIndex;
this.baseOptions = param.baseOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import type { Configuration } from "./configuration";
import type { RequestArgs } from "./base";
import type { AxiosInstance, AxiosResponse } from 'axios';
import type { AxiosInstance, AxiosResponse } from "axios";
import { RequiredError } from "./base";

/**
Expand Down Expand Up @@ -84,6 +84,7 @@ export const setOAuthToObject = async function (object: any, name: string, scope
}
}


function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
if (parameter == null) return;
if (typeof parameter === "object") {
Expand Down
Loading