Skip to content

Commit

Permalink
Add httpAuthSchemeMiddleware to select an auth scheme (#929)
Browse files Browse the repository at this point in the history
  • Loading branch information
Steven Yuan authored Sep 20, 2023
1 parent c346d59 commit 76e2ef3
Show file tree
Hide file tree
Showing 15 changed files with 554 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-games-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/experimental-identity-and-auth": patch
---

Allow `DefaultIdentityProviderConfig` to accept `undefined` in the constructor
5 changes: 5 additions & 0 deletions .changeset/moody-actors-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/experimental-identity-and-auth": patch
---

Add `httpAuthSchemeMiddleware` to select an auth scheme
5 changes: 5 additions & 0 deletions .changeset/slow-pillows-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/experimental-identity-and-auth": patch
---

Add `memoizeIdentityProvider()`
1 change: 1 addition & 0 deletions packages/experimental-identity-and-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@smithy/middleware-endpoint": "workspace:^",
"@smithy/middleware-retry": "workspace:^",
"@smithy/protocol-http": "workspace:^",
"@smithy/signature-v4": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface IdentityProviderConfig {
}

/**
* Default implementation of IddentityProviderConfig
* Default implementation of IdentityProviderConfig
* @internal
*/
export class DefaultIdentityProviderConfig implements IdentityProviderConfig {
Expand All @@ -27,9 +27,11 @@ export class DefaultIdentityProviderConfig implements IdentityProviderConfig {
*
* @param config scheme IDs and identity providers to configure
*/
constructor(config: Record<HttpAuthSchemeId, IdentityProvider<Identity>>) {
constructor(config: Record<HttpAuthSchemeId, IdentityProvider<Identity> | undefined>) {
for (const [key, value] of Object.entries(config)) {
this.authSchemes.set(key, value);
if (value !== undefined) {
this.authSchemes.set(key, value);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/experimental-identity-and-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export * from "./apiKeyIdentity";
export * from "./createEndpointRuleSetHttpAuthSchemeProvider";
export * from "./httpApiKeyAuth";
export * from "./httpBearerAuth";
export * from "./memoizeIdentityProvider";
export * from "./middleware-http-auth-scheme";
export * from "./middleware-http-signing";
export * from "./noAuth";
export * from "./tokenIdentity";
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Identity, IdentityProvider } from "@smithy/types";

/**
* @internal
* This may need to be configurable in the future, but for now it is defaulted to 5min.
*/
export const EXPIRATION_MS = 300_000;

/**
* @internal
*/
export const isIdentityExpired = (identity: Identity) =>
doesIdentityRequireRefresh(identity) && identity.expiration!.getTime() - Date.now() < EXPIRATION_MS;

/**
* @internal
*/
export const doesIdentityRequireRefresh = (identity: Identity) => identity.expiration !== undefined;

/**
* @internal
*/
export interface MemoizedIdentityProvider<IdentityT extends Identity> {
(options?: Record<string, any> & { forceRefresh?: boolean }): Promise<IdentityT>;
}

/**
* @internal
*/
export const memoizeIdentityProvider = <IdentityT extends Identity>(
provider: IdentityT | IdentityProvider<IdentityT> | undefined,
isExpired: (resolved: Identity) => boolean,
requiresRefresh: (resolved: Identity) => boolean
): MemoizedIdentityProvider<IdentityT> | undefined => {
if (provider === undefined) {
return undefined;
}
const normalizedProvider: IdentityProvider<IdentityT> =
typeof provider !== "function" ? async () => Promise.resolve(provider) : provider;
let resolved: IdentityT;
let pending: Promise<IdentityT> | undefined;
let hasResult: boolean;
let isConstant = false;
// Wrapper over supplied provider with side effect to handle concurrent invocation.
const coalesceProvider: MemoizedIdentityProvider<IdentityT> = async (options) => {
if (!pending) {
pending = normalizedProvider(options);
}
try {
resolved = await pending;
hasResult = true;
isConstant = false;
} finally {
pending = undefined;
}
return resolved;
};

if (isExpired === undefined) {
// This is a static memoization; no need to incorporate refreshing unless using forceRefresh;
return async (options) => {
if (!hasResult || options?.forceRefresh) {
resolved = await coalesceProvider(options);
}
return resolved;
};
}

return async (options) => {
if (!hasResult || options?.forceRefresh) {
resolved = await coalesceProvider(options);
}
if (isConstant) {
return resolved;
}

if (!requiresRefresh(resolved)) {
isConstant = true;
return resolved;
}
if (isExpired(resolved)) {
await coalesceProvider(options);
return resolved;
}
return resolved;
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { endpointMiddlewareOptions } from "@smithy/middleware-endpoint";
import { MetadataBearer, Pluggable, RelativeMiddlewareOptions, SerializeHandlerOptions } from "@smithy/types";

import { httpAuthSchemeMiddleware, PreviouslyResolved } from "./httpAuthSchemeMiddleware";

/**
* @internal
*/
export const httpAuthSchemeMiddlewareOptions: SerializeHandlerOptions & RelativeMiddlewareOptions = {
step: "serialize",
tags: ["HTTP_AUTH_SCHEME"],
name: "httpAuthSchemeMiddleware",
override: true,
relation: "before",
toMiddleware: endpointMiddlewareOptions.name!,
};

/**
* @internal
*/
export const getHttpAuthSchemePlugin = <
Input extends Record<string, unknown> = Record<string, unknown>,
Output extends MetadataBearer = MetadataBearer
>(
config: PreviouslyResolved
): Pluggable<Input, Output> => ({
applyToStack: (clientStack) => {
clientStack.addRelativeTo(httpAuthSchemeMiddleware(config), httpAuthSchemeMiddlewareOptions);
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
HandlerExecutionContext,
MetadataBearer,
SerializeHandler,
SerializeHandlerArguments,
SerializeHandlerOutput,
SerializeMiddleware,
SMITHY_CONTEXT_KEY,
} from "@smithy/types";
import { getSmithyContext } from "@smithy/util-middleware";

import { HttpAuthScheme, HttpAuthSchemeId, SelectedHttpAuthScheme } from "../HttpAuthScheme";
import { HttpAuthSchemeParametersProvider, HttpAuthSchemeProvider } from "../HttpAuthSchemeProvider";
import { IdentityProviderConfig } from "../IdentityProviderConfig";

/**
* @internal
*/
export interface PreviouslyResolved {
httpAuthSchemes: HttpAuthScheme[];
httpAuthSchemeProvider: HttpAuthSchemeProvider;
httpAuthSchemeParametersProvider: HttpAuthSchemeParametersProvider;
identityProviderConfig: IdentityProviderConfig;
}

/**
* @internal
*/
interface HttpAuthSchemeMiddlewareSmithyContext extends Record<string, unknown> {
selectedHttpAuthScheme?: SelectedHttpAuthScheme;
}

/**
* @internal
*/
interface HttpAuthSchemeMiddlewareHandlerExecutionContext extends HandlerExecutionContext {
[SMITHY_CONTEXT_KEY]?: HttpAuthSchemeMiddlewareSmithyContext;
}

/**
* @internal
* Later HttpAuthSchemes with the same HttpAuthSchemeId will overwrite previous ones.
*/
function convertHttpAuthSchemesToMap(httpAuthSchemes: HttpAuthScheme[]): Map<HttpAuthSchemeId, HttpAuthScheme> {
const map = new Map();
for (const scheme of httpAuthSchemes) {
map.set(scheme.schemeId, scheme);
}
return map;
}

/**
* @internal
*/
export const httpAuthSchemeMiddleware = <
Input extends Record<string, unknown> = Record<string, unknown>,
Output extends MetadataBearer = MetadataBearer
>(
config: PreviouslyResolved
): SerializeMiddleware<Input, Output> => (
next: SerializeHandler<Input, Output>,
context: HttpAuthSchemeMiddlewareHandlerExecutionContext
): SerializeHandler<Input, Output> => async (
args: SerializeHandlerArguments<Input>
): Promise<SerializeHandlerOutput<Output>> => {
const options = config.httpAuthSchemeProvider(
await config.httpAuthSchemeParametersProvider(config, context, args.input)
);
const authSchemes = convertHttpAuthSchemesToMap(config.httpAuthSchemes);
const smithyContext: HttpAuthSchemeMiddlewareSmithyContext = getSmithyContext(context);
const failureReasons = [];
for (const option of options) {
const scheme = authSchemes.get(option.schemeId);
if (!scheme) {
failureReasons.push(`HttpAuthScheme \`${option.schemeId}\` was not enable for this service.`);
continue;
}
const identityProvider = scheme.identityProvider(config.identityProviderConfig);
if (!identityProvider) {
failureReasons.push(`HttpAuthScheme \`${option.schemeId}\` did not have an IdentityProvider configured.`);
continue;
}
const identity = await identityProvider(option.identityProperties || {});
smithyContext.selectedHttpAuthScheme = {
httpAuthOption: option,
identity,
signer: scheme.signer,
};
break;
}
if (!smithyContext.selectedHttpAuthScheme) {
throw new Error(failureReasons.join("\n"));
}
return next(args);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./httpAuthSchemeMiddleware";
export * from "./getHttpAuthSchemePlugin";
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,10 @@
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.codegen.core.SymbolReference;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.knowledge.TopDownIndex;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
import software.amazon.smithy.typescript.codegen.auth.AuthUtils;
import software.amazon.smithy.typescript.codegen.auth.http.HttpAuthScheme;
import software.amazon.smithy.typescript.codegen.auth.http.SupportedHttpAuthSchemesIndex;
import software.amazon.smithy.typescript.codegen.endpointsV2.EndpointsV2Generator;
import software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin;
import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration;
Expand Down Expand Up @@ -310,42 +305,10 @@ private void generateClientDefaults() {
+ "trait of an operation.");
writer.write("disableHostPrefix?: boolean;\n");

// feat(experimentalIdentityAndAuth): write httpAuthSchemes and httpAuthSchemeProvider into ClientDefaults
if (settings.getExperimentalIdentityAndAuth()) {
writer.addDependency(TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
writer.addImport("HttpAuthScheme", null, TypeScriptDependency.EXPERIMENTAL_IDENTITY_AND_AUTH);
writer.writeDocs("""
experimentalIdentityAndAuth: Configuration of HttpAuthSchemes for a client which provides \
default identity providers and signers per auth scheme.
@internal""");
writer.write("httpAuthSchemes?: HttpAuthScheme[];\n");

String httpAuthSchemeProviderName = service.toShapeId().getName() + "HttpAuthSchemeProvider";
writer.addImport(httpAuthSchemeProviderName, null, AuthUtils.AUTH_HTTP_PROVIDER_DEPENDENCY);
writer.writeDocs("""
experimentalIdentityAndAuth: Configuration of an HttpAuthSchemeProvider for a client which \
resolves which HttpAuthScheme to use.
@internal""");
writer.write("httpAuthSchemeProvider?: $L;\n", httpAuthSchemeProviderName);
}

// Write custom configuration dependencies.
for (TypeScriptIntegration integration : integrations) {
integration.addConfigInterfaceFields(settings, model, symbolProvider, writer);
}

// feat(experimentalIdentityAndAuth): write any HttpAuthScheme config fields into ClientDefaults
// WARNING: may be changed later in lieu of {@link TypeScriptIntegration#addConfigInterfaceFields()},
// but will depend after HttpAuthScheme integration implementations.
if (settings.getExperimentalIdentityAndAuth()) {
Map<ShapeId, HttpAuthScheme> httpAuthSchemes = AuthUtils.getAllEffectiveNoAuthAwareAuthSchemes(
service, ServiceIndex.of(model), new SupportedHttpAuthSchemesIndex(integrations));
Map<String, ConfigField> configFields = AuthUtils.collectConfigFields(httpAuthSchemes.values());
for (ConfigField configField : configFields.values()) {
writer.writeDocs(() -> writer.write("$C", configField.docs()));
writer.write("$L?: $C;\n", configField.name(), configField.inputType());
}
}
}).write("");
}

Expand Down
Loading

0 comments on commit 76e2ef3

Please sign in to comment.