Repository: smithy-lang/smithy
Pull request #1929 (2023-08-16 19:05:46)

Add validator for services with no auth trait
Adds a validator that warns when there's a service shape that has multiple authDefinition traits applied, but no auth trait. This encourages the use of the auth trait so auth scheme priority is being modeled explicitly. It also allows for tooling to look for and use this validation event.

Documentation was also updated to explain that without the auth trait, auth scheme ordering is alphabetical.

A test for the auth trait (auth-trait-must-target-service-schemes) was updated to avoid getting the warning message from the new validator.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
new file mode 100644
index 000000000..c590ca2a1
--- /dev/null
+++ b/.github/workflows/stale-issues.yml
@@ -0,0 +1,43 @@
+name: "Close stale issues"
+
+# Controls when the action will run.
+on:
+  schedule:
+    - cron: "0 0 * * *"
+
+jobs:
+  cleanup:
+    runs-on: ubuntu-latest
+    name: Stale issue job
+    steps:
+      - uses: aws-actions/stale-issue-cleanup@v3
+        with:
+          # Targeted types
+          issue-types: issues
+
+          # Issue timing
+          days-before-stale: 60
+          days-before-close: 14
+          days-before-ancient: 365
+
+          # Labels
+          stale-issue-label: closing-soon
+          exempt-issue-label: bug,documentation,feature-request,investigating,protocol-test
+          response-requested-label: response-requested
+
+          # Messages
+          stale-issue-message: This issue has not received a response in a while. If you still think there is a problem, please leave a comment to avoid the issue from automatically closing.
+          ancient-issue-message: We’re closing this issue because it has been open a long time and hasn’t been updated in a while and may not be getting the attention it deserves. We encourage you to check if this is still an issue in the latest release and if you find that this is still a problem, please feel free to comment or open a new issue.
+
+          # If you don't want to mark a issue as being ancient based on a
+          # threshold of "upvotes", you can set this here. An "upvote" is
+          # the total number of +1, heart, hooray, and rocket reactions
+          # on an issue.
+          minimum-upvotes-to-exempt: 5
+
+          # Don't set closed-for-staleness label to skip closing very old issues
+          # regardless of label
+          closed-for-staleness-label: closed-for-staleness
+
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          loglevel: DEBUG
diff --git a/docs/source-2.0/spec/authentication-traits.rst b/docs/source-2.0/spec/authentication-traits.rst
index d1d0b3e16..446560d86 100644
--- a/docs/source-2.0/spec/authentication-traits.rst
+++ b/docs/source-2.0/spec/authentication-traits.rst
@@ -276,6 +276,11 @@ the ``auth`` trait, then the operation is expected to support each of the
 service. Each entry in the ``auth`` trait is a shape ID that MUST refer to an
 authentication scheme trait applied to the service in which it is bound.
 
+.. note::
+    When a service has multiple authentication scheme traits applied and no
+    ``auth`` trait, the ordering of authentication schemes is alphabetical
+    based on the absolute shape ID of each authentication scheme trait.
+
 The following example defines all combinations in which ``auth`` can be applied
 to services and operations:
 
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java
index 0c32dc009..7b7935df1 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/ServiceIndex.java
@@ -29,8 +29,11 @@ import software.amazon.smithy.model.shapes.ShapeId;
 import software.amazon.smithy.model.shapes.ToShapeId;
 import software.amazon.smithy.model.traits.AuthDefinitionTrait;
 import software.amazon.smithy.model.traits.AuthTrait;
+import software.amazon.smithy.model.traits.OptionalAuthTrait;
 import software.amazon.smithy.model.traits.ProtocolDefinitionTrait;
 import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
+import software.amazon.smithy.utils.MapUtils;
 
 /**
  * An index that resolves service protocols and auth schemes.
@@ -63,6 +66,25 @@ public final class ServiceIndex implements KnowledgeIndex {
         return model.getKnowledge(ServiceIndex.class, ServiceIndex::new);
     }
 
+     /**
+     * Defines the type of auth schemes returned by {@link #getEffectiveAuthSchemes}.
+     */
+    public enum AuthSchemeMode {
+
+        /**
+         * Use only the modeled auth schemes. This is the default.
+         */
+        MODELED,
+
+        /**
+         * Use the modeled auth schemes, as well as the synthetic {@link NoAuthTrait} where applicable.
+         *
+         * <p>The Smithy Reference Architecture recommends using the {@code smithy.api#noAuth} auth scheme to represent
+         * no authentication which is available as the {@link NoAuthTrait}.
+         */
+        NO_AUTH_AWARE;
+    }
+
     /**
      * Get all protocol traits attached to a service.
      *
@@ -105,6 +127,8 @@ public final class ServiceIndex implements KnowledgeIndex {
      * <p>An <em>auth defining trait</em> is a trait that is marked with
      * the {@code smithy.api#authDefinition} trait.
      *
+     * <p>The returned map is ordered alphabetically by absolute shape ID.
+     *
      * <p>An empty map is returned if {@code id} cannot be found in the
      * model or is not a service shape.
      *
@@ -127,7 +151,7 @@ public final class ServiceIndex implements KnowledgeIndex {
      *
      * <p>The returned map is provided in the same order as the values in the
      * {@code auth} trait if an auth trait is present, otherwise the result
-     * returned is ordered alphabetically by absolute shape id.
+     * returned is ordered alphabetically by absolute shape ID.
      *
      * <p>An empty map is returned if {@code service} cannot be found in the
      * model or is not a service shape.
@@ -154,6 +178,30 @@ public final class ServiceIndex implements KnowledgeIndex {
                 .orElse(Collections.emptyMap());
     }
 
+    /**
+     * Gets a list of effective authentication schemes applied to a service, based on the AuthSchemeMode.
+     *
+     * <p>If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as
+     * {@link #getEffectiveAuthSchemes(ToShapeId)}.
+     *
+     * <p>If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, except that if the service has no effective
+     * auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth} auth scheme. It avoids having to
+     * special case handling an empty result. The returned map will always contain at least 1 entry.
+     *
+     * @param service Service to get the effective authentication schemes of.
+     * @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include.
+     * @return Returns a map of the trait shape ID to the auth trait itself.
+     */
+    public Map<ShapeId, Trait> getEffectiveAuthSchemes(ToShapeId service, AuthSchemeMode authSchemeMode) {
+        Map<ShapeId, Trait> authSchemes = getEffectiveAuthSchemes(service);
+        if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) {
+            if (authSchemes.isEmpty()) {
+                authSchemes = MapUtils.of(NoAuthTrait.ID, new NoAuthTrait());
+            }
+        }
+        return authSchemes;
+    }
+
     /**
      * Gets a list of effective authentication schemes applied to an operation
      * bound within a service.
@@ -167,7 +215,7 @@ public final class ServiceIndex implements KnowledgeIndex {
      *
      * <p>The returned map is provided in the same order as the values in the
      * {@code auth} trait if an auth trait is present, otherwise the result
-     * returned is ordered alphabetically by absolute shape id.
+     * returned is ordered alphabetically by absolute shape ID.
      *
      * <p>An empty map is returned if {@code service} shape cannot be found
      * in the model or is not a service shape. An empty map is returned if
@@ -198,7 +246,49 @@ public final class ServiceIndex implements KnowledgeIndex {
                 .orElse(Collections.emptyMap());
     }
 
-    private Map<ShapeId, Trait> getAuthTraitValues(Shape service, Shape subject) {
+    /**
+     * Gets a list of effective authentication schemes applied to an operation
+     * bound within a service, based on the AuthSchemeMode.
+     *
+     * <p>If AuthSchemeMode is {@code MODELED}, which is the default, the behavior is same as
+     * {@link #getEffectiveAuthSchemes(ToShapeId, ToShapeId)}.
+     *
+     * <p>If AuthSchemeMode is {@code NO_AUTH_AWARE}, the behavior is same, with the following differences:
+     * If the operation has no effective auth schemes, instead of an empty map, it returns the {@code smithy.api#noAuth}
+     * auth scheme.
+     * If the operation has the {@code smithy.api#optionalAuth} trait, it adds {@code smithy.api#noAuth} to the end.
+     *
+     * <p>Using {@code NO_AUTH_AWARE} accounts for {@code smithy.api#optionalAuth} and avoids having to special case
+     * handling an empty result. The returned map will always contain at least 1 entry.
+     *
+     * <p>The {@code smithy.api#noAuth} scheme, if present, is always the last scheme.
+     *
+     * @param service Service the operation is within.
+     * @param operation Operation to get the effective authentication schemes of.
+     * @param authSchemeMode AuthSchemeMode to determine which authentication schemes to include.
+     * @return Returns a map of the trait shape ID to the auth trait itself.
+     */
+    public Map<ShapeId, Trait> getEffectiveAuthSchemes(ToShapeId service,
+                                                       ToShapeId operation,
+                                                       AuthSchemeMode authSchemeMode) {
+        Map<ShapeId, Trait> authSchemes = getEffectiveAuthSchemes(service, operation);
+        if (authSchemeMode == AuthSchemeMode.NO_AUTH_AWARE) {
+            if (authSchemes.isEmpty() || hasOptionalAuth(operation)) {
+                authSchemes = new LinkedHashMap<>(authSchemes);
+                authSchemes.put(NoAuthTrait.ID, new NoAuthTrait());
+            }
+        }
+        return authSchemes;
+    }
+
+    private boolean hasOptionalAuth(ToShapeId operation) {
+        return getModel()
+                .getShape(operation.toShapeId())
+                .filter(shape -> shape.hasTrait(OptionalAuthTrait.class))
+                .isPresent();
+    }
+
+    private static Map<ShapeId, Trait> getAuthTraitValues(Shape service, Shape subject) {
         if (!subject.hasTrait(AuthTrait.class)) {
             return null;
         }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java
new file mode 100644
index 000000000..d49a472e4
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/NoAuthTrait.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.smithy.model.traits.synthetic;
+
+import software.amazon.smithy.model.knowledge.ServiceIndex;
+import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.traits.AnnotationTrait;
+
+/**
+ * An auth scheme trait for {@code smithy.api#noAuth} which indicates no authentication. This is not a real trait
+ * in the semantic model, but a valid auth scheme for use in {@link ServiceIndex#getEffectiveAuthSchemes} with
+ * {@link AuthSchemeMode#NO_AUTH_AWARE}.
+ */
+public final class NoAuthTrait extends AnnotationTrait {
+
+    public static final ShapeId ID = ShapeId.from("smithy.api#noAuth");
+
+    public NoAuthTrait() {
+        super(ID, Node.objectNode());
+    }
+
+    @Override
+    public boolean isSynthetic() {
+        return true;
+    }
+}
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ServiceAuthDefinitionsValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ServiceAuthDefinitionsValidator.java
new file mode 100644
index 000000000..3e5965712
--- /dev/null
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ServiceAuthDefinitionsValidator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.smithy.model.validation.validators;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.knowledge.ServiceIndex;
+import software.amazon.smithy.model.shapes.ServiceShape;
+import software.amazon.smithy.model.traits.AuthTrait;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+
+/**
+ * Validates the @authDefinition traits applied to service shapes.
+ */
+public class ServiceAuthDefinitionsValidator extends AbstractValidator {
+
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        List<ValidationEvent> events = new ArrayList<>();
+        ServiceIndex index = ServiceIndex.of(model);
+
+        List<ServiceShape> services = model.getServiceShapes().stream()
+                .filter(serviceShape -> !serviceShape.hasTrait(AuthTrait.ID))
+                .filter(serviceShape -> index.getAuthSchemes(serviceShape).size() > 1)
+                .collect(Collectors.toList());
+
+        for (ServiceShape service : services) {
+            events.add(warning(service, "This service uses multiple authentication schemes but does not have "
+                                        + "the `@auth` trait applied. The `@auth` trait defines a priority ordering "
+                                        + "of auth schemes for a client to use. Without it, the ordering of auth "
+                                        + "schemes is alphabetical based on the absolute shape ID of the auth "
+                                        + "schemes."));
+
+        }
+        return events;
+    }
+}
diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index 391872dd4..f278bac44 100644
--- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -36,6 +36,7 @@ software.amazon.smithy.model.validation.validators.ResourceIdentifierBindingVali
 software.amazon.smithy.model.validation.validators.ResourceIdentifierValidator
 software.amazon.smithy.model.validation.validators.ResourceLifecycleValidator
 software.amazon.smithy.model.validation.validators.ResourceOperationInputOutputValidator
+software.amazon.smithy.model.validation.validators.ServiceAuthDefinitionsValidator
 software.amazon.smithy.model.validation.validators.ServiceValidator
 software.amazon.smithy.model.validation.validators.SetValidator
 software.amazon.smithy.model.validation.validators.ShapeIdConflictValidator


Test patch:
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java
index e7e899a6f..e58285e69 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/ServiceIndexTest.java
@@ -16,12 +16,13 @@
 package software.amazon.smithy.model.knowledge;
 
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.hasSize;
-import static org.hamcrest.Matchers.equalTo;
+import static software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode.NO_AUTH_AWARE;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import org.junit.jupiter.api.AfterAll;
@@ -33,9 +34,12 @@ import software.amazon.smithy.model.traits.HttpBasicAuthTrait;
 import software.amazon.smithy.model.traits.HttpBearerAuthTrait;
 import software.amazon.smithy.model.traits.HttpDigestAuthTrait;
 import software.amazon.smithy.model.traits.Trait;
+import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
 
 public class ServiceIndexTest {
 
+    private static final ShapeId CUSTOM_AUTH_ID = ShapeId.from("smithy.example#customAuth");
+
     private static Model model;
 
     @BeforeAll
@@ -52,7 +56,7 @@ public class ServiceIndexTest {
     }
 
     @Test
-    public void returnsProtocolsOfService() {
+    public void protocolsOfService() {
         Model model = Model.assembler()
                 .addImport(getClass().getResource("service-index-loads-protocols.smithy"))
                 .assemble()
@@ -66,88 +70,193 @@ public class ServiceIndexTest {
     }
 
     @Test
-    public void returnsAuthSchemesOfService() {
+    public void authSchemesOfService() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
         Map<ShapeId, Trait> auth = serviceIndex.getAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithNoAuthTrait"));
+                ShapeId.from("smithy.example#ServiceWithoutAuthTrait"));
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+    }
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(3));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
-        assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
+    @Test
+    public void authSchemesOfServiceWithoutAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
     }
 
     @Test
-    public void getsAuthSchemesOfServiceWithNoAuthTrait() {
+    public void authSchemesOfServiceWithAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithNoAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(3));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
-        assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
     }
 
     @Test
-    public void getsAuthSchemesOfServiceWithAuthTrait() {
+    public void authSchemesOfServiceWithEmptyAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(auth.keySet(), hasSize(2));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
     }
 
     @Test
-    public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithNoAuthTrait() {
+    public void authSchemesOfServiceWithoutAuthDefinitionTraits() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithNoAuthTrait"),
-                ShapeId.from("smithy.example#OperationWithNoAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthDefinitionTraits");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(3));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpBearerAuthTrait.ID));
-        assertThat(ids.get(2), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
     }
 
     @Test
-    public void getsAuthSchemesOfOperationWithNoAuthTraitAndServiceWithAuthTrait() {
+    public void authSchemesOfOperationWithoutAuthTraitAndServiceWithoutAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#ServiceWithAuthTrait"),
-                ShapeId.from("smithy.example#OperationWithNoAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");
 
-        List<ShapeId> ids = new ArrayList<>(auth.keySet());
-        assertThat(ids, hasSize(2));
-        assertThat(ids.get(0), equalTo(HttpBasicAuthTrait.ID));
-        assertThat(ids.get(1), equalTo(HttpDigestAuthTrait.ID));
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
     }
 
     @Test
-    public void getsAuthSchemesOfOperationWithAuthTrait() {
+    public void authSchemesOfOperationWithoutAuthTraitAndServiceWithAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfOperationWithoutAuthTraitAndServiceWithEmptyAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithEmptyAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithoutAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfOperationWithAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
         Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
                 ShapeId.from("smithy.example#ServiceWithAuthTrait"),
                 ShapeId.from("smithy.example#OperationWithAuthTrait"));
+        assertAuthSchemes(auth, HttpDigestAuthTrait.ID);
+    }
 
-        assertThat(auth.keySet(), hasSize(1));
-        assertThat(auth, hasKey(HttpDigestAuthTrait.ID));
+    @Test
+    public void authSchemesOfOperationWithEmptyAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithEmptyAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
     }
 
     @Test
-    public void returnsAnEmptyCollectionWhenTheServiceDoesNotExist() {
+    public void authSchemesOfOperationWithOptionalAuthTrait() {
         ServiceIndex serviceIndex = ServiceIndex.of(model);
-        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(
-                ShapeId.from("smithy.example#Invalid"),
-                ShapeId.from("smithy.example#OperationWithAuthTrait"));
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithOptionalAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpDigestAuthTrait.ID, NoAuthTrait.ID);
+    }
+
+    // Test to assert that smithy.api#noAuth trait is not part of traits that are sorted alphabetically, but last.
+    // The authSchemesOfOperationWithOptionalAuthTrait() test above doesn't really assert that, because
+    // smithy.api#noAuth would have been last if included in sorting.
+    @Test
+    public void authSchemesOfOperationWithOptionalAuthTraitAndServiceWithoutAuthTrait() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithOptionalAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, HttpBasicAuthTrait.ID, HttpBearerAuthTrait.ID, HttpDigestAuthTrait.ID, CUSTOM_AUTH_ID,
+                NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfInvalidService() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#Invalid");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service);
+        assertAuthSchemes(auth);
 
-        assertThat(auth.keySet(), empty());
+        auth = serviceIndex.getEffectiveAuthSchemes(service, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfInvalidServiceWithInvalidOperation() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#Invalid");
+        ShapeId operation = ShapeId.from("smithy.example#OperationWithAuthTrait");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    @Test
+    public void authSchemesOfServiceWithInvalidOperation() {
+        ServiceIndex serviceIndex = ServiceIndex.of(model);
+        ShapeId service = ShapeId.from("smithy.example#ServiceWithoutAuthTrait");
+        ShapeId operation = ShapeId.from("smithy.example#InvalidOperation");
+
+        Map<ShapeId, Trait> auth = serviceIndex.getEffectiveAuthSchemes(service, operation);
+        assertAuthSchemes(auth);
+
+        auth = serviceIndex.getEffectiveAuthSchemes(service, operation, NO_AUTH_AWARE);
+        assertAuthSchemes(auth, NoAuthTrait.ID);
+    }
+
+    private void assertAuthSchemes(Map<ShapeId, Trait> auth, ShapeId... authSchemes) {
+        List<ShapeId> ids = new ArrayList<>(auth.keySet());
+        assertThat(ids, hasSize(authSchemes.length));
+        assertThat(ids, equalTo(Arrays.asList(authSchemes)));
     }
 }
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/auth/auth-trait-must-target-service-schemes.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/auth/auth-trait-must-target-service-schemes.json
index 572a88684..ebfd40d15 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/auth/auth-trait-must-target-service-schemes.json
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/auth/auth-trait-must-target-service-schemes.json
@@ -10,7 +10,11 @@
             ],
             "traits": {
                 "smithy.api#httpBasicAuth": {},
-                "smithy.api#httpDigestAuth": {}
+                "smithy.api#httpDigestAuth": {},
+                "smithy.api#auth": [
+                    "smithy.api#httpDigestAuth",
+                    "smithy.api#httpBasicAuth"
+                ]
             }
         },
         "ns.foo#ValidOperation": {
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/multiple-auth-schemes.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/multiple-auth-schemes.errors
new file mode 100644
index 000000000..a4a677ed5
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/multiple-auth-schemes.errors
@@ -0,0 +1,1 @@
+[WARNING] smithy.example#FooService: This service uses multiple authentication schemes but does not have the `@auth` trait applied. The `@auth` trait defines a priority ordering of auth schemes for a client to use. Without it, the ordering of auth schemes is alphabetical based on the absolute shape ID of the auth schemes. | ServiceAuthDefinitions
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/multiple-auth-schemes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/multiple-auth-schemes.smithy
new file mode 100644
index 000000000..e188ee3f3
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/multiple-auth-schemes.smithy
@@ -0,0 +1,18 @@
+$version: "2"
+
+namespace smithy.example
+
+// Service shape with multiple auth schemes and no auth trait should cause warning
+@httpBasicAuth
+@httpDigestAuth
+@httpBearerAuth
+service FooService {
+    version: "2023-08-15"
+    operations: [GetFoo]
+}
+
+operation GetFoo {
+    output: GetFooOutput
+}
+
+structure GetFooOutput {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/one-auth-scheme.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/one-auth-scheme.errors
new file mode 100644
index 000000000..e69de29bb
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/one-auth-scheme.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/one-auth-scheme.smithy
new file mode 100644
index 000000000..5974d929b
--- /dev/null
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/service-auth-definitions/one-auth-scheme.smithy
@@ -0,0 +1,16 @@
+$version: "2"
+
+namespace smithy.example
+
+// Service shape with one auth schemes and no auth trait should NOT cause warning
+@httpBasicAuth
+service FooService {
+    version: "2023-08-15"
+    operations: [GetFoo]
+}
+
+operation GetFoo {
+    output: GetFooOutput
+}
+
+structure GetFooOutput {}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy
index 95d5d6c8a..1a2ccb168 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/service-index-finds-auth-schemes.smithy
@@ -2,30 +2,77 @@ $version: "2"
 
 namespace smithy.example
 
+@customAuth
 @httpBasicAuth
 @httpDigestAuth
 @httpBearerAuth
-service ServiceWithNoAuthTrait {
-    version: "2020-01-29",
+service ServiceWithoutAuthTrait {
+    version: "2020-01-29"
     operations: [
-        OperationWithNoAuthTrait,
+        OperationWithoutAuthTrait
         OperationWithAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
     ]
 }
 
+@customAuth
 @httpBasicAuth
 @httpDigestAuth
 @httpBearerAuth
 @auth([httpBasicAuth, httpDigestAuth])
 service ServiceWithAuthTrait {
-    version: "2020-01-29",
+    version: "2020-01-29"
     operations: [
-        OperationWithNoAuthTrait,
+        OperationWithoutAuthTrait
         OperationWithAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
     ]
 }
 
-operation OperationWithNoAuthTrait {}
+@customAuth
+@httpBasicAuth
+@httpDigestAuth
+@httpBearerAuth
+@auth([])
+service ServiceWithEmptyAuthTrait {
+    version: "2020-01-29"
+    operations: [
+        OperationWithoutAuthTrait
+        OperationWithAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
+    ]
+}
+
+service ServiceWithoutAuthDefinitionTraits {
+    version: "2020-01-29"
+    operations: [
+        OperationWithoutAuthTrait
+        OperationWithEmptyAuthTrait
+        OperationWithOptionalAuthTrait
+    ]
+}
+
+operation OperationWithoutAuthTrait {}
 
 @auth([httpDigestAuth])
 operation OperationWithAuthTrait {}
+
+@auth([])
+operation OperationWithEmptyAuthTrait {}
+
+@optionalAuth
+operation OperationWithOptionalAuthTrait {}
+
+// Defining a custom trait, to assert that alphabetical sorting of traits takes namespace into account, as well as
+// smithy.api#noAuth is added to the end, and not included in sorting.
+@trait(
+    selector: "service"
+    breakingChanges: [
+        {change: "remove"}
+    ]
+)
+@authDefinition
+structure customAuth {}