amplify-flutter icon indicating copy to clipboard operation
amplify-flutter copied to clipboard

Amplify Gen2 Flutter REST API

Open mateuszboryn opened this issue 1 year ago • 2 comments

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

  • [ ] Analytics
  • [X] API (REST)
  • [ ] API (GraphQL)
  • [ ] Auth
  • [ ] Authenticator
  • [ ] DataStore
  • [ ] Notifications (Push)
  • [ ] Storage

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

  • [ ] iOS
  • [X] Android
  • [ ] Web
  • [ ] macOS
  • [ ] Windows
  • [ ] Linux

Flutter Version

3.22.3

Amplify Flutter Version

2.3.0

Deployment Method

AWS CDK

Schema

No response

mateuszboryn avatar Aug 08 '24 11:08 mateuszboryn

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.

tyllark avatar Aug 09 '24 19:08 tyllark

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

  • It helped me to get IAM auth REST API working.
  • However, I'm missing AMAZON_COGNITO_USER_POOLS REST API (I have a strict requirement to avoid AWS access key and secret access key in the REST API).

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"}
            },
        }
    }
});

mateuszboryn avatar Aug 12 '24 06:08 mateuszboryn

I have to do this today to make it work, @Jordan-Nelson @NikaHsn could you confirm this is the current approach for output version 1.3?

``... final json = jsonDecode(amplifyConfig); final myRestApi= json['custom']['API']['myRestApi']; json['rest_api'] = { 'myRestApi': { 'aws_region': myRestApi['region'], 'url': myRestApi['endpoint'], } }; final configString = jsonEncode(json); await Amplify.configure(configString);

hangoocn avatar Nov 25 '24 16:11 hangoocn

Hi @mateuszboryn, yes that is correct as of the current working approach! We are still tracking the feature request however cannot provide an update yet.

ekjotmultani avatar Nov 26 '24 19:11 ekjotmultani

Thanks, I look forward for an updated official documentation.

mateuszboryn avatar Dec 05 '24 08:12 mateuszboryn