Describe the bug
My Amplify REACT application is calling an existing REST API, protected by Cognito User Pool. API has been created with SAM and works OK from Postman.
SAM code is :
AWSSessionFeedbackAPI:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors: "'*'"
Auth:
DefaultAuthorizer: MyCognitoAuthorizer
Authorizers:
MyCognitoAuthorizer:
UserPoolArn: !Ref CognitoUserPoolARN
Amplify code is
async getSessions() {
let apiName = API_NAME;
let path = '/session';
let myInit = {
headers: { Authorization: `Bearer ${(await Auth.currentSession()).accessToken.jwtToken}` }
}
return await API.get(apiName, path, myInit);
}
This call fails for two reasons.
1. OPTIONS HTTP call is not sending accessToken
The underlying Axios library issues an OPTIONS call before the GET. SAM / API Gateway creates the OPTIONS verb automatically and protects it with the same authentication as the GET verb.
Unfortunately, the Authorization header is not sent, so the API Gateway rejects that call with 401 unauthorized
Here is a copy a cURL output from the browser's network console
curl 'https://ycmi1l7r54.execute-api.eu-west-1.amazonaws.com/Prod/session' -X OPTIONS -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:67.0) Gecko/20100101 Firefox/67.0' -H 'Accept: */*' -H 'Accept-Language: en,fr;q=0.7,es;q=0.3' --compressed -H 'Access-Control-Request-Method: GET' -H 'Access-Control-Request-Headers: authorization' -H 'Origin: http://localhost:3000' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Referer: http://localhost:3000/session'
(no authorization header is sent)
Workaround 1 : disable authentication on the API OPTIONS verb. Is this safe ? Is this a best practice ?
2. OPTIONS call does not return Access-Control-Allow-Headers value
This is probably a problem to report to the SAM team instead.
The default setup of the API Gateway OPTIONS response adds the following HTTP headers :
Access-Control-Allow-Origin : *Access-Control-Allow-Methods: GET,OPTIONSBut, by CORS rules, the browser also expects to receive an explicit authorization to send the Authorization header in the GET call. So OPTIONS must also return :
Access-Control-Allow-Headers: authorizationFailure to do so result in a CORS error in the browser :
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://ycmi1l7r54.execute-api.eu-west-1.amazonaws.com/Prod/session. (Reason: missing token ‘authorization’ in CORS header ‘Access-Control-Allow-Headers’ from CORS preflight channel).
Workaround 2 : manually reconfigure the API Gateway OPTIONS integration response to add Access-Control-Allow-Headers: authorization to the HTTP headers.
Let me know how to best triage this second one, as I realize this is not related to Amplify.
After applying workaround #1 and workaround #2 manually to the API Gateway configuration, the above Amplify React code works as expected.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
amplify should work out of the box (i.e. no manual configuration / tweaking) with REST API protected by Cognito User Pool
Screenshots
n/a
Desktop (please complete the following information):
Smartphone (please complete the following information):
n/a
Additional context
contact me at [email protected] if you need more details.
After many searches, I finally found a doc explicitly syaing that CORS pre-flight requests MUST NOT be authenticated.
https://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
So, looks like 1/ above is not an issue with Amplify. I must configure my SAM template to ensure OPTIONS calls are not protected.
Given the above (OPTIONS call should not be authenticated) I am closing this issue. Here is a sample SAM template to correctly configure API Gateway
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-app
Sample SAM Template for sam-app
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 30
Parameters:
CognitoUserPoolARN:
Type: String
Default: arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${AWS::Region}_XYZ
Resources:
AWSSessionFeedbackAPI:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors:
AllowOrigin: "'*'"
Auth:
# DefaultAuthorizer: MyCognitoAuthorizer
Authorizers:
MyCognitoAuthorizer:
UserPoolArn: !Ref CognitoUserPoolARN
DefinitionBody:
swagger: "2.0"
info:
version: "1.0"
title: "aws-session-feedback"
securityDefinitions:
MyCognitoAuthorizer:
type: "apiKey"
name: "Authorization"
in: "header"
x-amazon-apigateway-authtype: "cognito_user_pools"
x-amazon-apigateway-authorizer:
providerARNs:
- "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${AWS::Region}_XYZ"
type: "cognito_user_pools"
paths:
/session:
get:
responses: {}
security:
- MyCognitoAuthorizer:
- "Sessionfeedback/Sessions"
x-amazon-apigateway-integration:
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AWSSessionFeedbackFunction.Arn}/invocations
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws_proxy"
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
'200':
description: "200 response"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-integration:
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
method.response.header.Access-Control-Allow-Headers: "'authorization'"
method.response.header.Access-Control-Allow-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
type: "mock"
AWSSessionFeedbackFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: session_feedback/
Handler: app.handler
Runtime: python3.7
Events:
OneSessions:
Type: Api
Properties:
RestApiId: !Ref AWSSessionFeedbackAPI
Path: /session
Method: get
Policies:
- Version: '2012-10-17' # Custom Policy to access DynamoDB
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:Query
Resource: !GetAtt AWSSessionFeedbackDatabase.Arn
AWSSessionFeedbackDatabase:
Type: 'AWS::DynamoDB::Table'
Properties:
TableName: aws-session-feedback
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
Most helpful comment
After many searches, I finally found a doc explicitly syaing that CORS pre-flight requests MUST NOT be authenticated.
https://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
So, looks like 1/ above is not an issue with Amplify. I must configure my SAM template to ensure OPTIONS calls are not protected.