Skip to content

Commit caf7e39

Browse files
authored
Add authorizationScopes trait for Cognito scopes
API Gateway supports OAuth scopes on operations that use Cognito authorizers. In OpenAPI, scopes appear in the security requirement array for each operation. The existing @Authorizer trait is a string and cannot be extended to include scopes without a breaking change. Add the `aws.apigateway#authorizationScopes` list trait, scoped to operations that have the @Authorizer trait applied. The existing `AddAuthorizers` mapper now includes the scope list in the security requirement when the trait is present. The mapper writes per-operation security when scopes are present, even if the operation inherits its authorizer from the service.
1 parent f4c2b53 commit caf7e39

10 files changed

Lines changed: 363 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"description": "Added `aws.apigateway#authorizationScopes` trait for Cognito authorizer scopes",
4+
"pull_requests": [
5+
"[#3084](https://github.com/smithy-lang/smithy/pull/3084)"
6+
]
7+
}

docs/source-2.0/aws/amazon-apigateway.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,72 @@ endpoint access mode:
584584
This trait should be considered internal-only and not exposed to your
585585
customers.
586586

587+
.. smithy-trait:: aws.apigateway#authorizationScopes
588+
.. _aws.apigateway#authorizationScopes-trait:
589+
590+
--------------------------------------------
591+
``aws.apigateway#authorizationScopes`` trait
592+
--------------------------------------------
593+
594+
Summary
595+
Defines the list of OAuth scopes required for an API Gateway operation
596+
that uses a `Cognito`_ authorizer. Applied alongside the
597+
:ref:`aws.apigateway#authorizer-trait` to specify which scopes the
598+
caller must have.
599+
Trait selector
600+
``operation[trait|aws.apigateway#authorizer]``
601+
602+
*An operation with the aws.apigateway#authorizer trait applied*
603+
Value type
604+
``list`` of ``string``
605+
See also
606+
- `Control access using Cognito user pools`_ for more information on
607+
how scopes work with Cognito authorizers
608+
609+
.. note::
610+
611+
Authorization scopes are only supported with ``COGNITO_USER_POOLS``
612+
authorizers. API Gateway validates the scope values at import time.
613+
614+
The following example requires the ``email`` and ``profile`` scopes on an
615+
operation that uses a Cognito authorizer:
616+
617+
.. code-block:: smithy
618+
619+
$version: "2"
620+
621+
namespace smithy.example
622+
623+
use aws.apigateway#authorizer
624+
use aws.apigateway#authorizers
625+
use aws.apigateway#authorizationScopes
626+
use aws.auth#sigv4
627+
628+
@sigv4(name: "service")
629+
@authorizer("my-cognito-auth")
630+
@authorizers(
631+
"my-cognito-auth": {
632+
scheme: "aws.auth#sigv4"
633+
type: "cognito_user_pools"
634+
}
635+
)
636+
service MyService {
637+
version: "2024-01-01"
638+
operations: [GetUserProfile]
639+
}
640+
641+
@authorizer("my-cognito-auth")
642+
@authorizationScopes(["email", "profile"])
643+
operation GetUserProfile {
644+
input := {}
645+
output := {}
646+
}
647+
648+
.. note::
649+
650+
This trait should be considered internal-only and not exposed to your
651+
customers.
652+
587653
.. smithy-trait:: aws.apigateway#integration
588654
.. _aws.apigateway#integration-trait:
589655

@@ -1268,3 +1334,5 @@ integration response to two ``header`` parameters of the method response.
12681334
.. _x-amazon-apigateway-endpoint-configuration: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-endpoint-configuration.html
12691335
.. _API endpoint types for REST APIs: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html
12701336
.. _endpoint IDs: https://docs.aws.amazon.com/vpc/latest/privatelink/concepts.html#concepts-vpc-endpoints
1337+
.. _Control access using Cognito user pools: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html
1338+
.. _Cognito: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools.html

docs/source-2.0/guides/model-translations/converting-to-openapi.rst

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2316,6 +2316,73 @@ is converted to the following OpenAPI model:
23162316
}
23172317
23182318
2319+
.. _apigateway-authorization-scopes:
2320+
2321+
Authorization scopes
2322+
====================
2323+
2324+
When an operation uses a `Cognito`_ authorizer, OAuth scopes can be added to
2325+
the security requirement using the
2326+
:ref:`aws.apigateway#authorizationScopes-trait`. The trait is applied
2327+
alongside the :ref:`aws.apigateway#authorizer-trait` on an operation and
2328+
specifies which scopes the caller must have.
2329+
2330+
The following Smithy model requires the ``email`` and ``profile`` scopes
2331+
on the ``GetUserProfile`` operation:
2332+
2333+
.. code-block:: smithy
2334+
2335+
$version: "2"
2336+
namespace smithy.example
2337+
2338+
use aws.apigateway#authorizer
2339+
use aws.apigateway#authorizers
2340+
use aws.apigateway#authorizationScopes
2341+
use aws.auth#sigv4
2342+
use aws.protocols#restJson1
2343+
2344+
@restJson1
2345+
@sigv4(name: "service")
2346+
@authorizer("my-cognito-auth")
2347+
@authorizers(
2348+
"my-cognito-auth": {scheme: "aws.auth#sigv4", type: "cognito_user_pools"}
2349+
)
2350+
service Example {
2351+
version: "2019-06-17"
2352+
operations: [GetUserProfile]
2353+
}
2354+
2355+
@authorizer("my-cognito-auth")
2356+
@authorizationScopes(["email", "profile"])
2357+
@http(uri: "/profile", method: "GET")
2358+
operation GetUserProfile {}
2359+
2360+
The scopes are included in the OpenAPI security requirement for the
2361+
operation:
2362+
2363+
.. code-block:: json
2364+
2365+
{
2366+
"paths": {
2367+
"/profile": {
2368+
"get": {
2369+
"operationId": "GetUserProfile",
2370+
"responses": {
2371+
"200": {
2372+
"description": "GetUserProfile response"
2373+
}
2374+
},
2375+
"security": [
2376+
{
2377+
"my-cognito-auth": ["email", "profile"]
2378+
}
2379+
]
2380+
}
2381+
}
2382+
}
2383+
}
2384+
2385+
23192386
.. _other-traits:
23202387

23212388
Other traits that influence API Gateway
@@ -2422,3 +2489,4 @@ The conversion process is highly extensible through
24222489
.. _x-amazon-apigateway-gateway-responses: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-gateway-responses.html
24232490
.. _x-amazon-apigateway-security-policy: https://docs.aws.amazon.com/apigateway/latest/developerguide/openapi-extensions-security-policy.html
24242491
.. _x-amazon-apigateway-endpoint-access-mode: https://docs.aws.amazon.com/apigateway/latest/developerguide/openapi-extensions-endpoint-access-mode.html
2492+
.. _Cognito: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools.html

smithy-aws-apigateway-openapi/src/main/java/software/amazon/smithy/aws/apigateway/openapi/AddAuthorizers.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Objects;
1010
import java.util.Optional;
1111
import java.util.logging.Logger;
12+
import software.amazon.smithy.aws.apigateway.traits.AuthorizationScopesTrait;
1213
import software.amazon.smithy.aws.apigateway.traits.AuthorizerDefinition;
1314
import software.amazon.smithy.aws.apigateway.traits.AuthorizerIndex;
1415
import software.amazon.smithy.aws.apigateway.traits.AuthorizerTrait;
@@ -106,12 +107,18 @@ public OperationObject updateOperation(
106107
// ...API Gateway's built-in API keys are being used. It requires the
107108
// security to be specified on every operation.
108109
// See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-setup-api-key-with-console.html#api-gateway-usage-plan-configure-apikey-on-method
109-
if (Objects.equals(operationAuth, serviceAuth) && !usesApiGatewayApiKeys(service, operationAuth)) {
110+
List<String> scopes = shape.getTrait(AuthorizationScopesTrait.class)
111+
.map(AuthorizationScopesTrait::getValues)
112+
.orElse(ListUtils.of());
113+
114+
if (Objects.equals(operationAuth, serviceAuth)
115+
&& !usesApiGatewayApiKeys(service, operationAuth)
116+
&& scopes.isEmpty()) {
110117
return operation;
111118
}
112119

113120
return operation.toBuilder()
114-
.addSecurity(MapUtils.of(operationAuth, ListUtils.of()))
121+
.addSecurity(MapUtils.of(operationAuth, scopes))
115122
.build();
116123
}
117124

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.aws.apigateway.openapi;
6+
7+
import static org.hamcrest.MatcherAssert.assertThat;
8+
import static org.hamcrest.Matchers.contains;
9+
import static org.hamcrest.Matchers.hasKey;
10+
import static org.hamcrest.Matchers.is;
11+
12+
import java.util.List;
13+
import java.util.Map;
14+
import org.junit.jupiter.api.BeforeAll;
15+
import org.junit.jupiter.api.Test;
16+
import software.amazon.smithy.model.Model;
17+
import software.amazon.smithy.model.shapes.ShapeId;
18+
import software.amazon.smithy.openapi.OpenApiConfig;
19+
import software.amazon.smithy.openapi.fromsmithy.OpenApiConverter;
20+
import software.amazon.smithy.openapi.model.OpenApi;
21+
import software.amazon.smithy.openapi.model.OperationObject;
22+
import software.amazon.smithy.openapi.model.PathItem;
23+
24+
public class AddAuthorizationScopesTest {
25+
private static OpenApi result;
26+
27+
@BeforeAll
28+
public static void setup() {
29+
Model model = Model.assembler()
30+
.discoverModels(AddAuthorizationScopesTest.class.getClassLoader())
31+
.addImport(AddAuthorizationScopesTest.class.getResource("authorization-scopes.smithy"))
32+
.assemble()
33+
.unwrap();
34+
OpenApiConfig config = new OpenApiConfig();
35+
config.setService(ShapeId.from("smithy.example#Service"));
36+
result = OpenApiConverter.create()
37+
.config(config)
38+
.classLoader(AddAuthorizationScopesTest.class.getClassLoader())
39+
.convert(model);
40+
}
41+
42+
@Test
43+
public void scopedOperationIncludesScopes() {
44+
PathItem path = result.getPaths().get("/scoped");
45+
OperationObject operation = path.getGet().get();
46+
List<Map<String, List<String>>> security = operation.getSecurity().get();
47+
48+
// Find the security requirement for our authorizer
49+
Map<String, List<String>> authReq = security.stream()
50+
.filter(s -> s.containsKey("my-cognito-auth"))
51+
.findFirst()
52+
.get();
53+
54+
assertThat(authReq, hasKey("my-cognito-auth"));
55+
assertThat(authReq.get("my-cognito-auth"), contains("email", "profile"));
56+
}
57+
58+
@Test
59+
public void unscopedOperationInheritsServiceSecurity() {
60+
PathItem path = result.getPaths().get("/unscoped");
61+
OperationObject operation = path.getGet().get();
62+
63+
// Unscoped operation has the same authorizer as the service and no
64+
// scopes, so no per-operation security is added. The operation
65+
// inherits security from the service level.
66+
assertThat(operation.getSecurity().isPresent(), is(false));
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
$version: "2.0"
2+
3+
namespace smithy.example
4+
5+
use aws.apigateway#authorizer
6+
use aws.apigateway#authorizers
7+
use aws.apigateway#authorizationScopes
8+
use aws.auth#sigv4
9+
use aws.protocols#restJson1
10+
11+
@restJson1
12+
@sigv4(name: "service")
13+
@authorizer("my-cognito-auth")
14+
@authorizers(
15+
"my-cognito-auth": {scheme: "aws.auth#sigv4", type: "cognito_user_pools", uri: "arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_abc123"}
16+
)
17+
service Service {
18+
version: "2006-03-01"
19+
operations: [ScopedOperation, UnscopedOperation]
20+
}
21+
22+
@authorizer("my-cognito-auth")
23+
@authorizationScopes(["email", "profile"])
24+
@http(uri: "/scoped", method: "GET")
25+
operation ScopedOperation {}
26+
27+
@http(uri: "/unscoped", method: "GET")
28+
operation UnscopedOperation {}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.aws.apigateway.traits;
6+
7+
import java.util.List;
8+
import software.amazon.smithy.model.FromSourceLocation;
9+
import software.amazon.smithy.model.shapes.ShapeId;
10+
import software.amazon.smithy.model.traits.StringListTrait;
11+
import software.amazon.smithy.utils.ToSmithyBuilder;
12+
13+
/**
14+
* Defines the list of OAuth scopes required for an API Gateway operation
15+
* that uses a Cognito authorizer. Applied alongside the
16+
* {@link AuthorizerTrait} to specify which scopes the caller must have.
17+
*/
18+
public final class AuthorizationScopesTrait extends StringListTrait
19+
implements ToSmithyBuilder<AuthorizationScopesTrait> {
20+
public static final ShapeId ID = ShapeId.from("aws.apigateway#authorizationScopes");
21+
22+
private AuthorizationScopesTrait(List<String> values, FromSourceLocation sourceLocation) {
23+
super(ID, values, sourceLocation);
24+
}
25+
26+
public static Builder builder() {
27+
return new Builder();
28+
}
29+
30+
@Override
31+
public Builder toBuilder() {
32+
return builder().sourceLocation(getSourceLocation()).values(getValues());
33+
}
34+
35+
public static final class Provider extends StringListTrait.Provider<AuthorizationScopesTrait> {
36+
public Provider() {
37+
super(ID, AuthorizationScopesTrait::new);
38+
}
39+
}
40+
41+
public static final class Builder extends StringListTrait.Builder<AuthorizationScopesTrait, Builder> {
42+
private Builder() {}
43+
44+
@Override
45+
public AuthorizationScopesTrait build() {
46+
return new AuthorizationScopesTrait(getValues(), getSourceLocation());
47+
}
48+
}
49+
}

smithy-aws-apigateway-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ software.amazon.smithy.aws.apigateway.traits.RequestValidatorTrait$Provider
44
software.amazon.smithy.aws.apigateway.traits.ApiKeySourceTrait$Provider
55
software.amazon.smithy.aws.apigateway.traits.IntegrationTrait$Provider
66
software.amazon.smithy.aws.apigateway.traits.MockIntegrationTrait$Provider
7+
software.amazon.smithy.aws.apigateway.traits.AuthorizationScopesTrait$Provider
78
software.amazon.smithy.aws.apigateway.traits.GatewayResponsesTrait$Provider
89
software.amazon.smithy.aws.apigateway.traits.ApiKeyRequiredTrait$Provider
910
software.amazon.smithy.aws.apigateway.traits.MinimumCompressionSizeTrait$Provider

smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.smithy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ structure apiKeyRequired {}
2727
@trait(selector: ":test(service, resource, operation)")
2828
string authorizer
2929

30+
/// Defines the list of OAuth scopes required for an operation that uses a
31+
/// Cognito authorizer. Applied alongside the @authorizer trait.
32+
@internal
33+
@tags(["internal"])
34+
@trait(selector: "operation[trait|aws.apigateway#authorizer]")
35+
list authorizationScopes {
36+
member: String
37+
}
38+
3039
/// A list of API Gateway authorizers to augment the service's declared authentication
3140
/// mechanisms.
3241
@internal

0 commit comments

Comments
 (0)