Skip to content

Commit 76e2ef3

Browse files
author
Steven Yuan
authored
Add httpAuthSchemeMiddleware to select an auth scheme (#929)
1 parent c346d59 commit 76e2ef3

File tree

15 files changed

+554
-52
lines changed

15 files changed

+554
-52
lines changed

.changeset/giant-games-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/experimental-identity-and-auth": patch
3+
---
4+
5+
Allow `DefaultIdentityProviderConfig` to accept `undefined` in the constructor

.changeset/moody-actors-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/experimental-identity-and-auth": patch
3+
---
4+
5+
Add `httpAuthSchemeMiddleware` to select an auth scheme

.changeset/slow-pillows-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/experimental-identity-and-auth": patch
3+
---
4+
5+
Add `memoizeIdentityProvider()`

packages/experimental-identity-and-auth/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"license": "Apache-2.0",
2525
"dependencies": {
26+
"@smithy/middleware-endpoint": "workspace:^",
2627
"@smithy/middleware-retry": "workspace:^",
2728
"@smithy/protocol-http": "workspace:^",
2829
"@smithy/signature-v4": "workspace:^",

packages/experimental-identity-and-auth/src/IdentityProviderConfig.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface IdentityProviderConfig {
1616
}
1717

1818
/**
19-
* Default implementation of IddentityProviderConfig
19+
* Default implementation of IdentityProviderConfig
2020
* @internal
2121
*/
2222
export class DefaultIdentityProviderConfig implements IdentityProviderConfig {
@@ -27,9 +27,11 @@ export class DefaultIdentityProviderConfig implements IdentityProviderConfig {
2727
*
2828
* @param config scheme IDs and identity providers to configure
2929
*/
30-
constructor(config: Record<HttpAuthSchemeId, IdentityProvider<Identity>>) {
30+
constructor(config: Record<HttpAuthSchemeId, IdentityProvider<Identity> | undefined>) {
3131
for (const [key, value] of Object.entries(config)) {
32-
this.authSchemes.set(key, value);
32+
if (value !== undefined) {
33+
this.authSchemes.set(key, value);
34+
}
3335
}
3436
}
3537

packages/experimental-identity-and-auth/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export * from "./apiKeyIdentity";
77
export * from "./createEndpointRuleSetHttpAuthSchemeProvider";
88
export * from "./httpApiKeyAuth";
99
export * from "./httpBearerAuth";
10+
export * from "./memoizeIdentityProvider";
11+
export * from "./middleware-http-auth-scheme";
1012
export * from "./middleware-http-signing";
1113
export * from "./noAuth";
1214
export * from "./tokenIdentity";
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Identity, IdentityProvider } from "@smithy/types";
2+
3+
/**
4+
* @internal
5+
* This may need to be configurable in the future, but for now it is defaulted to 5min.
6+
*/
7+
export const EXPIRATION_MS = 300_000;
8+
9+
/**
10+
* @internal
11+
*/
12+
export const isIdentityExpired = (identity: Identity) =>
13+
doesIdentityRequireRefresh(identity) && identity.expiration!.getTime() - Date.now() < EXPIRATION_MS;
14+
15+
/**
16+
* @internal
17+
*/
18+
export const doesIdentityRequireRefresh = (identity: Identity) => identity.expiration !== undefined;
19+
20+
/**
21+
* @internal
22+
*/
23+
export interface MemoizedIdentityProvider<IdentityT extends Identity> {
24+
(options?: Record<string, any> & { forceRefresh?: boolean }): Promise<IdentityT>;
25+
}
26+
27+
/**
28+
* @internal
29+
*/
30+
export const memoizeIdentityProvider = <IdentityT extends Identity>(
31+
provider: IdentityT | IdentityProvider<IdentityT> | undefined,
32+
isExpired: (resolved: Identity) => boolean,
33+
requiresRefresh: (resolved: Identity) => boolean
34+
): MemoizedIdentityProvider<IdentityT> | undefined => {
35+
if (provider === undefined) {
36+
return undefined;
37+
}
38+
const normalizedProvider: IdentityProvider<IdentityT> =
39+
typeof provider !== "function" ? async () => Promise.resolve(provider) : provider;
40+
let resolved: IdentityT;
41+
let pending: Promise<IdentityT> | undefined;
42+
let hasResult: boolean;
43+
let isConstant = false;
44+
// Wrapper over supplied provider with side effect to handle concurrent invocation.
45+
const coalesceProvider: MemoizedIdentityProvider<IdentityT> = async (options) => {
46+
if (!pending) {
47+
pending = normalizedProvider(options);
48+
}
49+
try {
50+
resolved = await pending;
51+
hasResult = true;
52+
isConstant = false;
53+
} finally {
54+
pending = undefined;
55+
}
56+
return resolved;
57+
};
58+
59+
if (isExpired === undefined) {
60+
// This is a static memoization; no need to incorporate refreshing unless using forceRefresh;
61+
return async (options) => {
62+
if (!hasResult || options?.forceRefresh) {
63+
resolved = await coalesceProvider(options);
64+
}
65+
return resolved;
66+
};
67+
}
68+
69+
return async (options) => {
70+
if (!hasResult || options?.forceRefresh) {
71+
resolved = await coalesceProvider(options);
72+
}
73+
if (isConstant) {
74+
return resolved;
75+
}
76+
77+
if (!requiresRefresh(resolved)) {
78+
isConstant = true;
79+
return resolved;
80+
}
81+
if (isExpired(resolved)) {
82+
await coalesceProvider(options);
83+
return resolved;
84+
}
85+
return resolved;
86+
};
87+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { endpointMiddlewareOptions } from "@smithy/middleware-endpoint";
2+
import { MetadataBearer, Pluggable, RelativeMiddlewareOptions, SerializeHandlerOptions } from "@smithy/types";
3+
4+
import { httpAuthSchemeMiddleware, PreviouslyResolved } from "./httpAuthSchemeMiddleware";
5+
6+
/**
7+
* @internal
8+
*/
9+
export const httpAuthSchemeMiddlewareOptions: SerializeHandlerOptions & RelativeMiddlewareOptions = {
10+
step: "serialize",
11+
tags: ["HTTP_AUTH_SCHEME"],
12+
name: "httpAuthSchemeMiddleware",
13+
override: true,
14+
relation: "before",
15+
toMiddleware: endpointMiddlewareOptions.name!,
16+
};
17+
18+
/**
19+
* @internal
20+
*/
21+
export const getHttpAuthSchemePlugin = <
22+
Input extends Record<string, unknown> = Record<string, unknown>,
23+
Output extends MetadataBearer = MetadataBearer
24+
>(
25+
config: PreviouslyResolved
26+
): Pluggable<Input, Output> => ({
27+
applyToStack: (clientStack) => {
28+
clientStack.addRelativeTo(httpAuthSchemeMiddleware(config), httpAuthSchemeMiddlewareOptions);
29+
},
30+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
HandlerExecutionContext,
3+
MetadataBearer,
4+
SerializeHandler,
5+
SerializeHandlerArguments,
6+
SerializeHandlerOutput,
7+
SerializeMiddleware,
8+
SMITHY_CONTEXT_KEY,
9+
} from "@smithy/types";
10+
import { getSmithyContext } from "@smithy/util-middleware";
11+
12+
import { HttpAuthScheme, HttpAuthSchemeId, SelectedHttpAuthScheme } from "../HttpAuthScheme";
13+
import { HttpAuthSchemeParametersProvider, HttpAuthSchemeProvider } from "../HttpAuthSchemeProvider";
14+
import { IdentityProviderConfig } from "../IdentityProviderConfig";
15+
16+
/**
17+
* @internal
18+
*/
19+
export interface PreviouslyResolved {
20+
httpAuthSchemes: HttpAuthScheme[];
21+
httpAuthSchemeProvider: HttpAuthSchemeProvider;
22+
httpAuthSchemeParametersProvider: HttpAuthSchemeParametersProvider;
23+
identityProviderConfig: IdentityProviderConfig;
24+
}
25+
26+
/**
27+
* @internal
28+
*/
29+
interface HttpAuthSchemeMiddlewareSmithyContext extends Record<string, unknown> {
30+
selectedHttpAuthScheme?: SelectedHttpAuthScheme;
31+
}
32+
33+
/**
34+
* @internal
35+
*/
36+
interface HttpAuthSchemeMiddlewareHandlerExecutionContext extends HandlerExecutionContext {
37+
[SMITHY_CONTEXT_KEY]?: HttpAuthSchemeMiddlewareSmithyContext;
38+
}
39+
40+
/**
41+
* @internal
42+
* Later HttpAuthSchemes with the same HttpAuthSchemeId will overwrite previous ones.
43+
*/
44+
function convertHttpAuthSchemesToMap(httpAuthSchemes: HttpAuthScheme[]): Map<HttpAuthSchemeId, HttpAuthScheme> {
45+
const map = new Map();
46+
for (const scheme of httpAuthSchemes) {
47+
map.set(scheme.schemeId, scheme);
48+
}
49+
return map;
50+
}
51+
52+
/**
53+
* @internal
54+
*/
55+
export const httpAuthSchemeMiddleware = <
56+
Input extends Record<string, unknown> = Record<string, unknown>,
57+
Output extends MetadataBearer = MetadataBearer
58+
>(
59+
config: PreviouslyResolved
60+
): SerializeMiddleware<Input, Output> => (
61+
next: SerializeHandler<Input, Output>,
62+
context: HttpAuthSchemeMiddlewareHandlerExecutionContext
63+
): SerializeHandler<Input, Output> => async (
64+
args: SerializeHandlerArguments<Input>
65+
): Promise<SerializeHandlerOutput<Output>> => {
66+
const options = config.httpAuthSchemeProvider(
67+
await config.httpAuthSchemeParametersProvider(config, context, args.input)
68+
);
69+
const authSchemes = convertHttpAuthSchemesToMap(config.httpAuthSchemes);
70+
const smithyContext: HttpAuthSchemeMiddlewareSmithyContext = getSmithyContext(context);
71+
const failureReasons = [];
72+
for (const option of options) {
73+
const scheme = authSchemes.get(option.schemeId);
74+
if (!scheme) {
75+
failureReasons.push(`HttpAuthScheme \`${option.schemeId}\` was not enable for this service.`);
76+
continue;
77+
}
78+
const identityProvider = scheme.identityProvider(config.identityProviderConfig);
79+
if (!identityProvider) {
80+
failureReasons.push(`HttpAuthScheme \`${option.schemeId}\` did not have an IdentityProvider configured.`);
81+
continue;
82+
}
83+
const identity = await identityProvider(option.identityProperties || {});
84+
smithyContext.selectedHttpAuthScheme = {
85+
httpAuthOption: option,
86+
identity,
87+
signer: scheme.signer,
88+
};
89+
break;
90+
}
91+
if (!smithyContext.selectedHttpAuthScheme) {
92+
throw new Error(failureReasons.join("\n"));
93+
}
94+
return next(args);
95+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./httpAuthSchemeMiddleware";
2+
export * from "./getHttpAuthSchemePlugin";

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServiceBareBonesClientGenerator.java

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,10 @@
2828
import software.amazon.smithy.codegen.core.SymbolProvider;
2929
import software.amazon.smithy.codegen.core.SymbolReference;
3030
import software.amazon.smithy.model.Model;
31-
import software.amazon.smithy.model.knowledge.ServiceIndex;
3231
import software.amazon.smithy.model.knowledge.TopDownIndex;
3332
import software.amazon.smithy.model.shapes.OperationShape;
3433
import software.amazon.smithy.model.shapes.ServiceShape;
35-
import software.amazon.smithy.model.shapes.ShapeId;
3634
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
37-
import software.amazon.smithy.typescript.codegen.auth.AuthUtils;
38-
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthScheme;
39-
import software.amazon.smithy.typescript.codegen.auth.http.SupportedHttpAuthSchemesIndex;
4035
import software.amazon.smithy.typescript.codegen.endpointsV2.EndpointsV2Generator;
4136
import software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin;
4237
import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration;
@@ -310,42 +305,10 @@ private void generateClientDefaults() {
310305
+ "trait of an operation.");
311306
writer.write("disableHostPrefix?: boolean;\n");
312307

313-
// feat(experimentalIdentityAndAuth): write httpAuthSchemes and httpAuthSchemeProvider into ClientDefaults
314-
if (settings.getExperimentalIdentityAndAuth()) {
315-
writer.addDependency(TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
316-
writer.addImport("HttpAuthScheme", null, TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
317-
writer.writeDocs("""
318-
experimentalIdentityAndAuth: Configuration of HttpAuthSchemes for a client which provides \
319-
default identity providers and signers per auth scheme.
320-
@internal""");
321-
writer.write("httpAuthSchemes?: HttpAuthScheme[];\n");
322-
323-
String httpAuthSchemeProviderName = service.toShapeId().getName() + "HttpAuthSchemeProvider";
324-
writer.addImport(httpAuthSchemeProviderName, null, AuthUtils.AUTH_HTTP_PROVIDER_DEPENDENCY);
325-
writer.writeDocs("""
326-
experimentalIdentityAndAuth: Configuration of an HttpAuthSchemeProvider for a client which \
327-
resolves which HttpAuthScheme to use.
328-
@internal""");
329-
writer.write("httpAuthSchemeProvider?: $L;\n", httpAuthSchemeProviderName);
330-
}
331-
332308
// Write custom configuration dependencies.
333309
for (TypeScriptIntegration integration : integrations) {
334310
integration.addConfigInterfaceFields(settings, model, symbolProvider, writer);
335311
}
336-
337-
// feat(experimentalIdentityAndAuth): write any HttpAuthScheme config fields into ClientDefaults
338-
// WARNING: may be changed later in lieu of {@link TypeScriptIntegration#addConfigInterfaceFields()},
339-
// but will depend after HttpAuthScheme integration implementations.
340-
if (settings.getExperimentalIdentityAndAuth()) {
341-
Map<ShapeId, HttpAuthScheme> httpAuthSchemes = AuthUtils.getAllEffectiveNoAuthAwareAuthSchemes(
342-
service, ServiceIndex.of(model), new SupportedHttpAuthSchemesIndex(integrations));
343-
Map<String, ConfigField> configFields = AuthUtils.collectConfigFields(httpAuthSchemes.values());
344-
for (ConfigField configField : configFields.values()) {
345-
writer.writeDocs(() -> writer.write("$C", configField.docs()));
346-
writer.write("$L?: $C;\n", configField.name(), configField.inputType());
347-
}
348-
}
349312
}).write("");
350313
}
351314

0 commit comments

Comments
 (0)