Repository: smithy-lang/smithy-rs (language: rust)

Issue:
Custom Authentication Scheme Issue
# Description
When creating a custom authentication workflow via the orchestrator using a `RuntimePlugin` per [this discussion post](https://github.com/awslabs/smithy-rs/discussions/2887), the authentication plugin is overwritten by the model default. In my case this default is no auth, so the authentication token headers were never used and my requests were rejected from the server. I am able to work around this using an `Interceptor` per [this snippet](https://github.com/awslabs/smithy-rs/blob/bc418284516975461dc0bee44ea999361f5c95be/aws/rust-runtime/aws-runtime/src/invocation_id.rs#L95-L129), but per office hours discussion this is not the idiomatic way and the issue with `RutimePlugin`s should be resolved.
# Context
## Reproduction
* Create some simple authentication middleware via `RuntimePlugin` per the discussion post linked above
* Using the following test, evaluate the trace logs and examine the authentication workflow and the final (lack of) headers:
```
    struct TestEndpointResolver;

    impl<T> ResolveEndpoint<T> for TestEndpointResolver {
        fn resolve_endpoint(&self, _params: &T) -> aws_smithy_http::endpoint::Result {
            Ok(::aws_smithy_types::endpoint::Endpoint::builder()
                .url(ENDPOINT_URL_STR)
                .property(
                    "authSchemes",
                    vec![::aws_smithy_types::Document::from({
                        let mut out = ::std::collections::HashMap::<
                            String,
                            ::aws_smithy_types::Document,
                        >::new();
                        out.insert("name".to_string(), "schemeid".to_string().into());
                        out
                    })],
                )
                .build())
        }
    }
    #[tokio::test]
    async fn proxy_test() {
        tracing_subscriber::fmt::init();
        let (server, request) = capture_request(None);
        let config = Config::builder()
            .http_connector(server)
            .endpoint_resolver(TestEndpointResolver {})
            .runtime_plugin(super::AuthRuntimePlugin::new())
            .build();
        let client = Client::from_conf(config);

        let _ = dbg!(client.get_client_configuration().send().await);
        let r = request.expect_request();
        dbg!(&r);
        assert!(r.headers().is_empty());
    }
```
Run with `RUST_LOGS=trace cargo test proxy_test`
## Smithy version
v0.56.1
## Relevant modifications
N/A
## Environment/Deployment Peculiarities
Code running on a custom embedded Linux-based stack



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml
index 0faa2266e8..7d66e72675 100644
--- a/CHANGELOG.next.toml
+++ b/CHANGELOG.next.toml
@@ -468,3 +468,9 @@ message = "Clients now have a default async sleep implementation so that one doe
 references = ["smithy-rs#3071"]
 meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "client" }
 author = "jdisanti"
+
+[[smithy-rs]]
+message = "Enable custom auth schemes to work by changing the code generated auth options to be set at the client level at `DEFAULTS` priority."
+references = ["smithy-rs#3034", "smithy-rs#3087"]
+meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" }
+author = "rcoh"
diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt
index ed66fa67ff..46f070e18e 100644
--- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt
+++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt
@@ -35,24 +35,37 @@ import software.amazon.smithy.rust.codegen.core.util.getTrait
 import software.amazon.smithy.rust.codegen.core.util.hasEventStreamOperations
 import software.amazon.smithy.rust.codegen.core.util.hasTrait
 import software.amazon.smithy.rust.codegen.core.util.isInputEventStream
+import software.amazon.smithy.rust.codegen.core.util.thenSingletonListOf
 
 class SigV4AuthDecorator : ClientCodegenDecorator {
     override val name: String get() = "SigV4AuthDecorator"
     override val order: Byte = 0
 
+    private val sigv4a = "sigv4a"
+
+    private fun sigv4(runtimeConfig: RuntimeConfig) = writable {
+        val awsRuntimeAuthModule = AwsRuntimeType.awsRuntime(runtimeConfig).resolve("auth")
+        rust("#T", awsRuntimeAuthModule.resolve("sigv4::SCHEME_ID"))
+    }
+
+    private fun sigv4a(runtimeConfig: RuntimeConfig) = writable {
+        val awsRuntimeAuthModule = AwsRuntimeType.awsRuntime(runtimeConfig).resolve("auth")
+        featureGateBlock(sigv4a) {
+            rust("#T", awsRuntimeAuthModule.resolve("sigv4a::SCHEME_ID"))
+        }
+    }
+
     override fun authOptions(
         codegenContext: ClientCodegenContext,
         operationShape: OperationShape,
         baseAuthSchemeOptions: List<AuthSchemeOption>,
-    ): List<AuthSchemeOption> = baseAuthSchemeOptions + AuthSchemeOption.StaticAuthSchemeOption(SigV4Trait.ID) {
-        val awsRuntimeAuthModule = AwsRuntimeType.awsRuntime(codegenContext.runtimeConfig).resolve("auth")
-        rust("#T,", awsRuntimeAuthModule.resolve("sigv4::SCHEME_ID"))
-        if (codegenContext.serviceShape.supportedAuthSchemes().contains("sigv4a")) {
-            featureGateBlock("sigv4a") {
-                rust("#T", awsRuntimeAuthModule.resolve("sigv4a::SCHEME_ID"))
-            }
-            rust(",")
-        }
+    ): List<AuthSchemeOption> {
+        val supportsSigV4a = codegenContext.serviceShape.supportedAuthSchemes().contains(sigv4a)
+            .thenSingletonListOf { sigv4a(codegenContext.runtimeConfig) }
+        return baseAuthSchemeOptions + AuthSchemeOption.StaticAuthSchemeOption(
+            SigV4Trait.ID,
+            listOf(sigv4(codegenContext.runtimeConfig)) + supportsSigV4a,
+        )
     }
 
     override fun serviceRuntimePluginCustomizations(
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt
index 4f73d1d376..bb2f0c17f0 100644
--- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecorator.kt
@@ -97,9 +97,11 @@ class HttpAuthDecorator : ClientCodegenDecorator {
                 options.add(
                     StaticAuthSchemeOption(
                         schemeShapeId,
-                        writable {
-                            rustTemplate("$name,", *codegenScope)
-                        },
+                        listOf(
+                            writable {
+                                rustTemplate(name, *codegenScope)
+                            },
+                        ),
                     ),
                 )
             }
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/NoAuthDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/NoAuthDecorator.kt
index 38582fabcd..bc68c958c8 100644
--- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/NoAuthDecorator.kt
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/NoAuthDecorator.kt
@@ -12,6 +12,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.customize.AuthSchemeOpt
 import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
 import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
 import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
+import software.amazon.smithy.rust.codegen.core.rustlang.writable
 import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
 
 val noAuthSchemeShapeId: ShapeId = ShapeId.from("aws.smithy.rs#NoAuth")
@@ -30,10 +31,15 @@ class NoAuthDecorator : ClientCodegenDecorator {
         operationShape: OperationShape,
         baseAuthSchemeOptions: List<AuthSchemeOption>,
     ): List<AuthSchemeOption> = baseAuthSchemeOptions +
-        AuthSchemeOption.StaticAuthSchemeOption(noAuthSchemeShapeId) {
-            rustTemplate(
-                "#{NO_AUTH_SCHEME_ID},",
-                "NO_AUTH_SCHEME_ID" to noAuthModule(codegenContext).resolve("NO_AUTH_SCHEME_ID"),
-            )
-        }
+        AuthSchemeOption.StaticAuthSchemeOption(
+            noAuthSchemeShapeId,
+            listOf(
+                writable {
+                    rustTemplate(
+                        "#{NO_AUTH_SCHEME_ID}",
+                        "NO_AUTH_SCHEME_ID" to noAuthModule(codegenContext).resolve("NO_AUTH_SCHEME_ID"),
+                    )
+                },
+            ),
+        )
 }
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt
index 36a517bb18..bfbbb05698 100644
--- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customize/ClientCodegenDecorator.kt
@@ -30,7 +30,7 @@ sealed interface AuthSchemeOption {
     /** Auth scheme for the `StaticAuthSchemeOptionResolver` */
     data class StaticAuthSchemeOption(
         val schemeShapeId: ShapeId,
-        val constructor: Writable,
+        val constructor: List<Writable>,
     ) : AuthSchemeOption
 
     class CustomResolver(/* unimplemented */) : AuthSchemeOption
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/AuthOptionsPluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/AuthOptionsPluginGenerator.kt
new file mode 100644
index 0000000000..2fd61b3806
--- /dev/null
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/AuthOptionsPluginGenerator.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.rust.codegen.client.smithy.generators
+
+import software.amazon.smithy.model.knowledge.ServiceIndex
+import software.amazon.smithy.model.shapes.OperationShape
+import software.amazon.smithy.model.traits.OptionalAuthTrait
+import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
+import software.amazon.smithy.rust.codegen.client.smithy.customizations.noAuthSchemeShapeId
+import software.amazon.smithy.rust.codegen.client.smithy.customize.AuthSchemeOption
+import software.amazon.smithy.rust.codegen.core.rustlang.Writable
+import software.amazon.smithy.rust.codegen.core.rustlang.isEmpty
+import software.amazon.smithy.rust.codegen.core.rustlang.join
+import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
+import software.amazon.smithy.rust.codegen.core.rustlang.writable
+import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
+import software.amazon.smithy.rust.codegen.core.util.PANIC
+import software.amazon.smithy.rust.codegen.core.util.hasTrait
+import java.util.logging.Logger
+
+class AuthOptionsPluginGenerator(private val codegenContext: ClientCodegenContext) {
+    private val runtimeConfig = codegenContext.runtimeConfig
+
+    private val logger: Logger = Logger.getLogger(javaClass.name)
+    fun authPlugin(operationShape: OperationShape, authSchemeOptions: List<AuthSchemeOption>) = writable {
+        rustTemplate(
+            """
+            #{DefaultAuthOptionsPlugin}::new(vec![#{options}])
+
+            """,
+            "DefaultAuthOptionsPlugin" to RuntimeType.defaultAuthPlugin(runtimeConfig),
+            "options" to actualAuthSchemes(operationShape, authSchemeOptions).join(", "),
+        )
+    }
+
+    private fun actualAuthSchemes(operationShape: OperationShape, authSchemeOptions: List<AuthSchemeOption>): List<Writable> {
+        val out: MutableList<Writable> = mutableListOf()
+
+        var noSupportedAuthSchemes = true
+        val authSchemes = ServiceIndex.of(codegenContext.model)
+            .getEffectiveAuthSchemes(codegenContext.serviceShape, operationShape)
+
+        for (schemeShapeId in authSchemes.keys) {
+            val optionsForScheme = authSchemeOptions.filter {
+                when (it) {
+                    is AuthSchemeOption.CustomResolver -> false
+                    is AuthSchemeOption.StaticAuthSchemeOption -> {
+                        it.schemeShapeId == schemeShapeId
+                    }
+                }
+            }
+
+            if (optionsForScheme.isNotEmpty()) {
+                out.addAll(optionsForScheme.flatMap { (it as AuthSchemeOption.StaticAuthSchemeOption).constructor })
+                noSupportedAuthSchemes = false
+            } else {
+                logger.warning(
+                    "No auth scheme implementation available for $schemeShapeId. " +
+                        "The generated client will not attempt to use this auth scheme.",
+                )
+            }
+        }
+        if (operationShape.hasTrait<OptionalAuthTrait>() || noSupportedAuthSchemes) {
+            val authOption = authSchemeOptions.find {
+                it is AuthSchemeOption.StaticAuthSchemeOption && it.schemeShapeId == noAuthSchemeShapeId
+            }
+                ?: throw IllegalStateException("Missing 'no auth' implementation. This is a codegen bug.")
+            out += (authOption as AuthSchemeOption.StaticAuthSchemeOption).constructor
+        }
+        if (out.any { it.isEmpty() }) {
+            PANIC("Got an empty auth scheme constructor. This is a bug. $out")
+        }
+        return out
+    }
+}
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt
index 9a3793b766..04e03f1af8 100644
--- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationGenerator.kt
@@ -51,7 +51,8 @@ open class OperationGenerator(
         operationShape: OperationShape,
         codegenDecorator: ClientCodegenDecorator,
     ) {
-        val operationCustomizations = codegenDecorator.operationCustomizations(codegenContext, operationShape, emptyList())
+        val operationCustomizations =
+            codegenDecorator.operationCustomizations(codegenContext, operationShape, emptyList())
         renderOperationStruct(
             operationWriter,
             operationShape,
@@ -94,7 +95,8 @@ open class OperationGenerator(
                 "Operation" to symbolProvider.toSymbol(operationShape),
                 "OperationError" to errorType,
                 "OperationOutput" to outputType,
-                "HttpResponse" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::orchestrator::HttpResponse"),
+                "HttpResponse" to RuntimeType.smithyRuntimeApi(runtimeConfig)
+                    .resolve("client::orchestrator::HttpResponse"),
                 "SdkError" to RuntimeType.sdkError(runtimeConfig),
             )
             val additionalPlugins = writable {
@@ -102,6 +104,13 @@ open class OperationGenerator(
                     operationCustomizations,
                     OperationSection.AdditionalRuntimePlugins(operationCustomizations, operationShape),
                 )
+                rustTemplate(
+                    ".with_client_plugin(#{auth_plugin})",
+                    "auth_plugin" to AuthOptionsPluginGenerator(codegenContext).authPlugin(
+                        operationShape,
+                        authSchemeOptions,
+                    ),
+                )
             }
             rustTemplate(
                 """
@@ -157,11 +166,13 @@ open class OperationGenerator(
                 *codegenScope,
                 "Error" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::interceptors::context::Error"),
                 "InterceptorContext" to RuntimeType.interceptorContext(runtimeConfig),
-                "OrchestratorError" to RuntimeType.smithyRuntimeApi(runtimeConfig).resolve("client::orchestrator::error::OrchestratorError"),
+                "OrchestratorError" to RuntimeType.smithyRuntimeApi(runtimeConfig)
+                    .resolve("client::orchestrator::error::OrchestratorError"),
                 "RuntimePlugin" to RuntimeType.runtimePlugin(runtimeConfig),
                 "RuntimePlugins" to RuntimeType.runtimePlugins(runtimeConfig),
                 "StopPoint" to RuntimeType.smithyRuntime(runtimeConfig).resolve("client::orchestrator::StopPoint"),
-                "invoke_with_stop_point" to RuntimeType.smithyRuntime(runtimeConfig).resolve("client::orchestrator::invoke_with_stop_point"),
+                "invoke_with_stop_point" to RuntimeType.smithyRuntime(runtimeConfig)
+                    .resolve("client::orchestrator::invoke_with_stop_point"),
                 "additional_runtime_plugins" to writable {
                     if (additionalPlugins.isNotEmpty()) {
                         rustTemplate(
@@ -182,7 +193,6 @@ open class OperationGenerator(
             operationWriter,
             operationShape,
             operationName,
-            authSchemeOptions,
             operationCustomizations,
         )
 
diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt
index 6782d2d8dc..226334d7d0 100644
--- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt
+++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/OperationRuntimePluginGenerator.kt
@@ -5,23 +5,15 @@
 
 package software.amazon.smithy.rust.codegen.client.smithy.generators
 
-import software.amazon.smithy.model.knowledge.ServiceIndex
 import software.amazon.smithy.model.shapes.OperationShape
-import software.amazon.smithy.model.traits.OptionalAuthTrait
 import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
-import software.amazon.smithy.rust.codegen.client.smithy.customizations.noAuthSchemeShapeId
-import software.amazon.smithy.rust.codegen.client.smithy.customize.AuthSchemeOption
 import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
-import software.amazon.smithy.rust.codegen.core.rustlang.Writable
 import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
-import software.amazon.smithy.rust.codegen.core.rustlang.withBlockTemplate
 import software.amazon.smithy.rust.codegen.core.rustlang.writable
 import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
 import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
 import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations
 import software.amazon.smithy.rust.codegen.core.util.dq
-import software.amazon.smithy.rust.codegen.core.util.hasTrait
-import java.util.logging.Logger
 
 /**
  * Generates operation-level runtime plugins
@@ -29,7 +21,6 @@ import java.util.logging.Logger
 class OperationRuntimePluginGenerator(
     private val codegenContext: ClientCodegenContext,
 ) {
-    private val logger: Logger = Logger.getLogger(javaClass.name)
     private val codegenScope = codegenContext.runtimeConfig.let { rc ->
         val runtimeApi = RuntimeType.smithyRuntimeApi(rc)
         val smithyTypes = RuntimeType.smithyTypes(rc)
@@ -57,7 +48,6 @@ class OperationRuntimePluginGenerator(
         writer: RustWriter,
         operationShape: OperationShape,
         operationStructName: String,
-        authSchemeOptions: List<AuthSchemeOption>,
         customizations: List<OperationCustomization>,
     ) {
         writer.rustTemplate(
@@ -80,7 +70,6 @@ class OperationRuntimePluginGenerator(
                 fn runtime_components(&self, _: &#{RuntimeComponentsBuilder}) -> #{Cow}<'_, #{RuntimeComponentsBuilder}> {
                     #{Cow}::Owned(
                         #{RuntimeComponentsBuilder}::new(${operationShape.id.name.dq()})
-                            #{auth_options}
                             #{interceptors}
                             #{retry_classifiers}
                     )
@@ -91,7 +80,6 @@ class OperationRuntimePluginGenerator(
             """,
             *codegenScope,
             *preludeScope,
-            "auth_options" to generateAuthOptions(operationShape, authSchemeOptions),
             "additional_config" to writable {
                 writeCustomizations(
                     customizations,
@@ -122,55 +110,4 @@ class OperationRuntimePluginGenerator(
             },
         )
     }
-
-    private fun generateAuthOptions(
-        operationShape: OperationShape,
-        authSchemeOptions: List<AuthSchemeOption>,
-    ): Writable = writable {
-        if (authSchemeOptions.any { it is AuthSchemeOption.CustomResolver }) {
-            throw IllegalStateException("AuthSchemeOption.CustomResolver is unimplemented")
-        } else {
-            withBlockTemplate(
-                """
-                .with_auth_scheme_option_resolver(#{Some}(
-                    #{SharedAuthSchemeOptionResolver}::new(
-                        #{StaticAuthSchemeOptionResolver}::new(vec![
-                """,
-                "]))))",
-                *codegenScope,
-            ) {
-                var noSupportedAuthSchemes = true
-                val authSchemes = ServiceIndex.of(codegenContext.model)
-                    .getEffectiveAuthSchemes(codegenContext.serviceShape, operationShape)
-
-                for (schemeShapeId in authSchemes.keys) {
-                    val optionsForScheme = authSchemeOptions.filter {
-                        when (it) {
-                            is AuthSchemeOption.CustomResolver -> false
-                            is AuthSchemeOption.StaticAuthSchemeOption -> {
-                                it.schemeShapeId == schemeShapeId
-                            }
-                        }
-                    }
-
-                    if (optionsForScheme.isNotEmpty()) {
-                        optionsForScheme.forEach { (it as AuthSchemeOption.StaticAuthSchemeOption).constructor(this) }
-                        noSupportedAuthSchemes = false
-                    } else {
-                        logger.warning(
-                            "No auth scheme implementation available for $schemeShapeId. " +
-                                "The generated client will not attempt to use this auth scheme.",
-                        )
-                    }
-                }
-                if (operationShape.hasTrait<OptionalAuthTrait>() || noSupportedAuthSchemes) {
-                    val authOption = authSchemeOptions.find {
-                        it is AuthSchemeOption.StaticAuthSchemeOption && it.schemeShapeId == noAuthSchemeShapeId
-                    }
-                        ?: throw IllegalStateException("Missing 'no auth' implementation. This is a codegen bug.")
-                    (authOption as AuthSchemeOption.StaticAuthSchemeOption).constructor(this)
-                }
-            }
-        }
-    }
 }
diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt
index 85bfc18593..56c9003b83 100644
--- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt
+++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/CargoDependency.kt
@@ -89,6 +89,8 @@ class InlineDependency(
         private fun forInlineableRustFile(name: String, vararg additionalDependencies: RustDependency) =
             forRustFile(RustModule.private(name), "/inlineable/src/$name.rs", *additionalDependencies)
 
+        fun defaultAuthPlugin(runtimeConfig: RuntimeConfig) = forInlineableRustFile("auth_plugin", CargoDependency.smithyRuntimeApi(runtimeConfig))
+
         fun jsonErrors(runtimeConfig: RuntimeConfig) =
             forInlineableRustFile(
                 "json_errors",
diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt
index d9b54d35b0..764f3b08c3 100644
--- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt
+++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt
@@ -169,6 +169,12 @@ fun <T : AbstractCodeWriter<T>> T.rust(
     this.write(contents.trim(), *args)
 }
 
+fun <T : AbstractCodeWriter<T>> T.rawRust(
+    @Language("Rust", prefix = "macro_rules! foo { () =>  {{\n", suffix = "\n}}}") contents: String,
+) {
+    this.write(escape(contents.trim()))
+}
+
 /**
  * Convenience wrapper that tells Intellij that the contents of this block are Rust
  */
diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt
index 7ccf2e9479..f728db76c5 100644
--- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt
+++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt
@@ -401,8 +401,10 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
         fun retryErrorKind(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("retry::ErrorKind")
         fun eventStreamReceiver(runtimeConfig: RuntimeConfig): RuntimeType =
             smithyHttp(runtimeConfig).resolve("event_stream::Receiver")
+
         fun eventStreamSender(runtimeConfig: RuntimeConfig): RuntimeType =
             smithyHttp(runtimeConfig).resolve("event_stream::EventStreamSender")
+
         fun futuresStreamCompatByteStream(runtimeConfig: RuntimeConfig): RuntimeType =
             smithyHttp(runtimeConfig).resolve("futures_stream_adapter::FuturesStreamCompatByteStream")
 
@@ -418,6 +420,10 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
         fun awsQueryCompatibleErrors(runtimeConfig: RuntimeConfig) =
             forInlineDependency(InlineDependency.awsQueryCompatibleErrors(runtimeConfig))
 
+        fun defaultAuthPlugin(runtimeConfig: RuntimeConfig) =
+            RuntimeType.forInlineDependency(InlineDependency.defaultAuthPlugin(runtimeConfig))
+                .resolve("DefaultAuthOptionsPlugin")
+
         fun labelFormat(runtimeConfig: RuntimeConfig, func: String) = smithyHttp(runtimeConfig).resolve("label::$func")
         fun operation(runtimeConfig: RuntimeConfig) = smithyHttp(runtimeConfig).resolve("operation::Operation")
         fun operationModule(runtimeConfig: RuntimeConfig) = smithyHttp(runtimeConfig).resolve("operation")
diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt
index f7a08f62f3..64223fe550 100644
--- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt
+++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/util/Exec.kt
@@ -8,10 +8,14 @@ package software.amazon.smithy.rust.codegen.core.util
 import java.io.IOException
 import java.nio.file.Path
 import java.util.concurrent.TimeUnit
+import java.util.logging.Logger
 
 data class CommandError(val output: String) : Exception("Command Error\n$output")
 
 fun String.runCommand(workdir: Path? = null, environment: Map<String, String> = mapOf(), timeout: Long = 3600): String {
+    val logger = Logger.getLogger("RunCommand")
+    logger.fine("Invoking comment $this in `$workdir` with env $environment")
+    val start = System.currentTimeMillis()
     val parts = this.split("\\s".toRegex())
     val builder = ProcessBuilder(*parts.toTypedArray())
         .redirectOutput(ProcessBuilder.Redirect.PIPE)
@@ -38,5 +42,8 @@ fun String.runCommand(workdir: Path? = null, environment: Map<String, String> =
         throw CommandError("$this was not a valid command.\n  Hint: is everything installed?\n$err")
     } catch (other: Exception) {
         throw CommandError("Unexpected exception thrown when executing subprocess:\n$other")
+    } finally {
+        val end = System.currentTimeMillis()
+        logger.fine("command duration: ${end - start}ms")
     }
 }
diff --git a/rust-runtime/inlineable/src/auth_plugin.rs b/rust-runtime/inlineable/src/auth_plugin.rs
new file mode 100644
index 0000000000..068eaa3da2
--- /dev/null
+++ b/rust-runtime/inlineable/src/auth_plugin.rs
@@ -0,0 +1,39 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+use std::borrow::Cow;
+
+use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
+use aws_smithy_runtime_api::client::auth::AuthSchemeId;
+use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
+use aws_smithy_runtime_api::client::runtime_plugin::{Order, RuntimePlugin};
+
+#[derive(Debug)]
+pub(crate) struct DefaultAuthOptionsPlugin {
+    runtime_components: RuntimeComponentsBuilder,
+}
+
+impl DefaultAuthOptionsPlugin {
+    pub(crate) fn new(auth_schemes: Vec<AuthSchemeId>) -> Self {
+        let runtime_components = RuntimeComponentsBuilder::new("default_auth_options")
+            .with_auth_scheme_option_resolver(Some(StaticAuthSchemeOptionResolver::new(
+                auth_schemes,
+            )));
+        Self { runtime_components }
+    }
+}
+
+impl RuntimePlugin for DefaultAuthOptionsPlugin {
+    fn order(&self) -> Order {
+        Order::Defaults
+    }
+
+    fn runtime_components(
+        &self,
+        _current_components: &RuntimeComponentsBuilder,
+    ) -> Cow<'_, RuntimeComponentsBuilder> {
+        Cow::Borrowed(&self.runtime_components)
+    }
+}
diff --git a/rust-runtime/inlineable/src/lib.rs b/rust-runtime/inlineable/src/lib.rs
index b672eeef9d..01490bcee5 100644
--- a/rust-runtime/inlineable/src/lib.rs
+++ b/rust-runtime/inlineable/src/lib.rs
@@ -26,6 +26,10 @@ mod serialization_settings;
 
 #[allow(unused)]
 mod endpoint_lib;
+
+#[allow(unused)]
+mod auth_plugin;
+
 // This test is outside of uuid.rs to enable copying the entirety of uuid.rs into the SDK without
 // requiring a proptest dependency
 #[cfg(test)]


Test patch:
diff --git a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecoratorTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecoratorTest.kt
index 76bb3f90b4..b2d7984b2d 100644
--- a/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecoratorTest.kt
+++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/customizations/HttpAuthDecoratorTest.kt
@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test
 import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
 import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
 import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
+import software.amazon.smithy.rust.codegen.core.rustlang.rawRust
 import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
 import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
 import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
@@ -98,6 +99,171 @@ class HttpAuthDecoratorTest {
         }
     }
 
+    @Test
+    fun noAuthCustomAuth() {
+        clientIntegrationTest(TestModels.noSchemes) { ctx, rustCrate ->
+            rustCrate.integrationTest("custom_auth_scheme_works") {
+                val moduleName = ctx.moduleUseName()
+                rawRust(
+                    """
+                    use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
+                    use aws_smithy_runtime_api::client::auth::AuthScheme;
+                    use aws_smithy_runtime_api::client::auth::{AuthSchemeId, Sign};
+                    use aws_smithy_runtime_api::client::identity::{
+                        IdentityFuture, ResolveIdentity, SharedIdentityResolver,
+                    };
+                    use aws_smithy_runtime_api::box_error::BoxError;
+                    use aws_smithy_runtime_api::client::auth::Signer;
+                    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
+                    use aws_smithy_runtime_api::client::runtime_components::{
+                        GetIdentityResolver, RuntimeComponentsBuilder,
+                    };
+                    use aws_smithy_runtime_api::client::identity::Identity;
+                    use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
+                    use aws_smithy_runtime_api::client::auth::AuthSchemeEndpointConfig;
+                    use aws_smithy_types::config_bag::ConfigBag;
+                    use std::borrow::Cow;
+                    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
+                    #[derive(Debug)]
+                    struct CustomAuthRuntimePlugin {
+                        components: RuntimeComponentsBuilder,
+                    }
+
+                    impl RuntimePlugin for CustomAuthRuntimePlugin {
+                        fn runtime_components(
+                            &self,
+                            _current_components: &RuntimeComponentsBuilder,
+                        ) -> Cow<'_, RuntimeComponentsBuilder> {
+                            Cow::Borrowed(&self.components)
+                        }
+                    }
+
+                    #[derive(Debug)]
+                    struct TestAuthScheme { signer: CustomSigner }
+
+                    impl AuthScheme for TestAuthScheme {
+                        fn scheme_id(&self) -> AuthSchemeId {
+                            AuthSchemeId::new("customauth")
+                        }
+
+                        fn identity_resolver(
+                            &self,
+                            identity_resolvers: &dyn GetIdentityResolver,
+                        ) -> Option<SharedIdentityResolver> {
+                            identity_resolvers.identity_resolver(self.scheme_id())
+                        }
+
+                        fn signer(&self) -> &dyn Sign {
+                            &self.signer
+                        }
+                    }
+
+                    #[derive(Debug, Default)]
+                    struct CustomSigner;
+
+                    #[derive(Debug)]
+                    struct CustomIdentity(String);
+
+                    impl Signer for CustomSigner {
+                        fn sign_http_request(
+                            &self,
+                            request: &mut HttpRequest,
+                            identity: &Identity,
+                            // In some advanced use cases, the Smithy `@endpointRuleSet` can
+                            // provide additional context in this config:
+                            _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
+                            _runtime_components: &RuntimeComponents,
+                            _config_bag: &ConfigBag,
+                        ) -> Result<(), BoxError> {
+                            // Downcast the identity to our known `Login` type
+                            let login = identity
+                                .data::<CustomIdentity>()
+                                .ok_or("custom auth requires a `Login` identity")?;
+                            // Use the login to add an authorization header to the request
+                            request.headers_mut().try_append(
+                                http::header::AUTHORIZATION,
+                                login.0.to_string()
+                            )?;
+                            Ok(())
+                        }
+                    }
+
+
+                    #[derive(Debug)]
+                    struct TestResolver;
+                    impl ResolveIdentity for TestResolver {
+                        fn resolve_identity<'a>(
+                            &'a self,
+                            _runtime_components: &'a RuntimeComponents,
+                            _config_bag: &'a ConfigBag,
+                        ) -> IdentityFuture<'a> {
+                            IdentityFuture::ready(Ok(Identity::new(CustomIdentity("password".to_string()), None)))
+                        }
+                    }
+
+                    impl CustomAuthRuntimePlugin {
+                        pub fn new() -> Self {
+                            let scheme_id = AuthSchemeId::new("customauth");
+                            Self {
+                                components: RuntimeComponentsBuilder::new("test-auth-scheme")
+                                    // Register our auth scheme
+                                    .with_auth_scheme(TestAuthScheme { signer: CustomSigner })
+                                    // Register our identity resolver with our auth scheme ID
+                                    .with_identity_resolver(
+                                        // This scheme ID needs to match the scheme ID returned in the auth scheme implementation
+                                        scheme_id,
+                                        TestResolver,
+                                    )
+                                    // Set the auth scheme option resolver to always use our basic auth auth scheme
+                                    .with_auth_scheme_option_resolver(Some(StaticAuthSchemeOptionResolver::new(vec![
+                                        scheme_id,
+                                    ]))),
+                            }
+                        }
+                    }
+
+                    #[test]
+                    fn compile() {}
+
+                    """,
+
+                )
+                Attribute.TokioTest.render(this)
+                rustTemplate(
+                    """
+                    async fn apply_custom_auth_scheme() {
+                        let (_guard, _rx) = #{capture_test_logs}();
+                        let http_client = #{StaticReplayClient}::new(
+                            vec![#{ReplayEvent}::new(
+                                http::Request::builder()
+                                    .header("authorization", "password")
+                                    .uri("http://localhost:1234/SomeOperation")
+                                    .body(#{SdkBody}::empty())
+                                    .unwrap(),
+                                http::Response::builder().status(200).body(#{SdkBody}::empty()).unwrap(),
+                            )],
+                        );
+                        let config = $moduleName::Config::builder()
+                            .endpoint_url("http://localhost:1234")
+                            .http_client(http_client.clone())
+                            .runtime_plugin(CustomAuthRuntimePlugin::new())
+                            .build();
+                        let client = $moduleName::Client::from_conf(config);
+                        let _req = dbg!(client.some_operation()
+                            .send()
+                            .await).expect("request should succeed");
+
+                        http_client.assert_requests_match(&[]);
+                    }
+                    """,
+                    "capture_test_logs" to CargoDependency.smithyRuntimeTestUtil(ctx.runtimeConfig).toType()
+                        .resolve("test_util::capture_test_logs::capture_test_logs"),
+                    *codegenScope(ctx.runtimeConfig),
+                )
+            }
+        }
+    }
+
     @Test
     fun apiKeyInQueryString() {
         clientIntegrationTest(TestModels.apiKeyInQueryString) { codegenContext, rustCrate ->
@@ -329,6 +495,29 @@ private object TestModels {
         }
     """.asSmithyModel()
 
+    val noSchemes = """
+        namespace test
+
+        use aws.api#service
+        use aws.protocols#restJson1
+
+        @service(sdkId: "Test Api Key Auth")
+        @restJson1
+        service TestService {
+            version: "2023-01-01",
+            operations: [SomeOperation]
+        }
+
+        structure SomeOutput {
+            someAttribute: Long,
+            someVal: String
+        }
+
+        @http(uri: "/SomeOperation", method: "GET")
+        operation SomeOperation {
+            output: SomeOutput
+        }""".asSmithyModel()
+
     val apiKeyInQueryString = """
         namespace test
 
diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt
index 4c1d435204..6970b9c284 100644
--- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt
+++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/testutil/CodegenIntegrationTest.kt
@@ -12,6 +12,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
 import software.amazon.smithy.rust.codegen.core.util.runCommand
 import java.io.File
 import java.nio.file.Path
+import java.util.logging.Logger
 
 /**
  * A helper class holding common data with defaults that is threaded through several functions, to make their
@@ -41,6 +42,8 @@ fun codegenIntegrationTest(model: Model, params: IntegrationTestParams, invokePl
     )
     invokePlugin(ctx)
     ctx.fileManifest.printGeneratedFiles()
-    params.command?.invoke(testDir) ?: (params.cargoCommand ?: "cargo test").runCommand(testDir, environment = mapOf("RUSTFLAGS" to "-D warnings"))
+    val logger = Logger.getLogger("CodegenIntegrationTest")
+    val out = params.command?.invoke(testDir) ?: (params.cargoCommand ?: "cargo test --lib --tests").runCommand(testDir, environment = mapOf("RUSTFLAGS" to "-D warnings"))
+    logger.fine(out.toString())
     return testDir
 }