aws-amplify / amplify-flutter

A declarative library with an easy-to-use interface for building Flutter applications on AWS.
https://docs.amplify.aws
Apache License 2.0
1.31k stars 241 forks source link

Amplify Gen2 Flutter REST API #5252

Open mateuszboryn opened 1 month ago

mateuszboryn commented 1 month ago

Description

I can't see an example of Amplify Gen2 Flutter REST API. There is one for React (https://docs.amplify.aws/react/build-a-backend/add-aws-services/rest-api/set-up-rest-api/), however whatever I try for Flutter, it doesn't work. Could you please provide some working example of:

  1. REST API Gateway CDK
  2. AND Dart code that invokes REST endpoint
  3. AND authorization using Cognito User pool for that endpoint

Categories

Steps to Reproduce

There's documentation for React (https://docs.amplify.aws/react/build-a-backend/add-aws-services/rest-api/set-up-rest-api/), but I can't find one for flutter. I tried to adapt existing example from React to Flutter, however I can't get requests authorized.

// backend.ts
// all the same as https://docs.amplify.aws/react/build-a-backend/add-aws-services/rest-api/set-up-rest-api/
// except for outputs, which is here, adjusted to what Flutter's Gen2 configure() expects:

// add outputs to the configuration file
backend.addOutput({
    custom: {
        rest_api: {
            [myRestApi.restApiName]: {
                aws_region: Stack.of(myRestApi).region,
                url: myRestApi.url,
                authorization_type: "AMAZON_COGNITO_USER_POOLS",
            },
        },
    }
});
// in _configureAmplify()
Map decodedAmplifyConfig = json.decode(amplifyConfig);
var newConfig = {};
newConfig.addAll(decodedAmplifyConfig);
newConfig.addAll(decodedAmplifyConfig["custom"]);
await Amplify.configure(json.encode(newConfig));
// call the API
try {
      final restOperation = Amplify.API.post(
        'cognito-auth-path',
        body: HttpPayload.json({'name': 'Mow the lawn'}),
      );
      final response = await restOperation.response;
      print('POST call succeeded');
      print(response.decodeBody());
    } on ApiException catch (e) {
      print('POST call failed: $e');
    }

The above gives 401

{"message":"Unauthorized"}

Screenshots

No response

Platforms

Flutter Version

3.22.3

Amplify Flutter Version

2.3.0

Deployment Method

AWS CDK

Schema

No response

tyllark commented 1 month ago

Hi @mateuszboryn thank you for opening this request. I'm going to mark this as a feature request for REST API support in Gen2.

In the meantime can you you try updating your amplify config Json like in this example

final json = jsonDecode(amplifyConfig);

// ignore: avoid_dynamic_calls
json['rest_api'] = {'multiAuthRest': json['custom']['multiAuthRest']};
final configString = jsonEncode(json);

await Amplify.configure(configString);

Let us know if this resolves your issue and please note that this is not an officially supported work around and could break in future versions, but we will let you know when we have an official implementation.

mateuszboryn commented 1 month ago

Thank you @tyllark for prompt response and the example provided.

The example you linked has name suggesting it is multiAuthRest. I searched the repo for multiAuthRest and the only resources created that I found are using IAM, and not COGNITO USER POOL.

With that example I got API Gateway to have routes with AWS_IAM authorizer. I'm expecting to have Cognito User Pool authorizer instead.

Below is the code that is much closer to what I expect with some comments on which settings fail.

// create a new REST API
const myRestApi = new RestApi(apiStack, "RestApi", {
    restApiName: "multiAuthRest",
    deploy: true,
    deployOptions: {
        stageName: "dev",
    },
    defaultCorsPreflightOptions: {
        allowOrigins: Cors.ALL_ORIGINS, // Restrict this to domains you trust
        allowMethods: Cors.ALL_METHODS, // Specify only the methods you need to allow
        allowHeaders: Cors.DEFAULT_HEADERS, // Specify only the headers you need to allow
    },
});

// create a new Lambda integration
const lambdaIntegration = new LambdaIntegration(
    backend.addDeviceFunction.resources.lambda
);

// const cognitoAuth = new CognitoUserPoolsAuthorizer(apiStack, "CognitoAuth", {    // uncomment only for authorizationType: AuthorizationType.COGNITO in resource below
//     cognitoUserPools: [backend.auth.resources.userPool],
// });

// create a new resource path with IAM authorization
const itemsPath = myRestApi.root.addResource("items", {
    defaultMethodOptions: {
        authorizationType: AuthorizationType.IAM,    // works, but relies on AWS Access key and secret access key.
        // authorizationType: AuthorizationType.COGNITO, authorizer: cognitoAuth,      // fails with 401 {"message":"Unauthorized"}
    },
});

// add methods you would like to create to the resource path
itemsPath.addMethod("ANY", lambdaIntegration);

// add a proxy resource path to the API
itemsPath.addProxy({
    anyMethod: true,
    defaultIntegration: lambdaIntegration,
});

const apiRestPolicy = new Policy(apiStack, "RestApiPolicy", {
    statements: [
        new PolicyStatement({
            actions: ["execute-api:Invoke"],
            resources: [
                `${myRestApi.arnForExecuteApi("*", "/items", "dev")}`,
                `${myRestApi.arnForExecuteApi("*", "/items/*", "dev")}`,
            ],
        }),
    ],
});

// attach the policy to the authenticated and unauthenticated IAM roles
backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(
    apiRestPolicy
);

// add outputs to the configuration file
backend.addOutput({
    custom: {
        rest_api: {
            [myRestApi.restApiName]: {
                url: myRestApi.url.replace(/\/+$/, ""),
                aws_region: Stack.of(myRestApi).region,
                authorization_type: AuthorizationType.IAM,       // works, but relies on AWS Access key and secret access key.

                // authorization_type: AuthorizationType.COGNITO, this gives error (apparently, string is not recognized in dart code mappings):
                // E/flutter ( 5190): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: ConfigurationError {
                // E/flutter ( 5190):   "message": "The provided configuration can not be decoded to AmplifyOutputs or AmplifyConfig. Check underlyingException.",
                // E/flutter ( 5190):   "recoverySuggestion": "If using Amplify Gen 2 ensure that the json string can be decoded to AmplifyOutputs type. If using Amplify Gen 1 ensure that the json string can be decoded to AmplifyConfig type.",
                // E/flutter ( 5190):   "underlyingException": "CheckedFromJsonException\nCould not create `AuthConfig`.\nThere is a problem with \"plugins\".\nNull is not a Map"
                // E/flutter ( 5190): }
                // E/flutter ( 5190): #0      AmplifyClass._parseJsonConfig (package:amplify_core/src/amplify_class.dart:151:9)
                // E/flutter ( 5190): #1      AmplifyClass.configure (package:amplify_core/src/amplify_class.dart:117:24)
                // E/flutter ( 5190): #2      _MyApp._configureAmplify (package:tta_user_app/main.dart:100:21)
                // E/flutter ( 5190): <asynchronous suspension>

                // authorization_type: "AMAZON_COGNITO_USER_POOLS",     // fails with 401 {"message":"Unauthorized"}
            },
        }
    }
});