Calling AWS APIGateway with a Cognito user pool - python

I have a REST API configured on my AWS account using AWS APIGateway. The REST API uses a Cognito Authorizer for authentication.
I am using a Python Script to interact with the API. In this Python script, I managed to authenticate a user. For the sake of informatio, here is how I do it handling the case where the user needs to update its password (first authentication) :
region = 'eu-west-1'
cognito = boto3.client('cognito-idp', region)
clientId = 'abcdefghijk27261819'
username = "myUsername"
password = "myPassword"
response = cognito.initiate_auth(AuthFlow='USER_PASSWORD_AUTH', AuthParameters={"USERNAME": username,
"PASSWORD": password}, ClientId = clientId)
try :
response['ChallengeName']
response = cognito.respond_to_auth_challenge(ClientId=clientId, ChallengeName=response['ChallengeName'], Session=response['Session'],
ChallengeResponses={"USERNAME": username,
"NEW_PASSWORD": "myNewPassword"})
except KeyError:
pass
# store credentials
accessToken = response['AuthenticationResult']['AccessToken']
idToken = response['AuthenticationResult']['IdToken']
I am able to verify that the identity token idToken is the one expected by the API through the APIGateway console :
However, even though the token seems to be ok, I can't call the API.
I tried two calls :
HOST = "https://<APIID>.execute-api.eu-west-1.amazonaws.com"
STAGE = "dev"
BASE_URL = f"{HOST}/{STAGE}"
r = requests.post(f"{BASE_URL}/S3Operations/CreateSimulationBucket", headers={"Authorization": idToken}) # the call I want
r = requests.post(f"{BASE_URL}", headers={"Authorization": idToken}) # the test call
and both calls return an error :
# first call
{'Content-Type': 'application/json', 'Content-Length': '23', 'Connection': 'keep-alive', 'Date': 'Tue, 22 Jun 2021 07:36:47 GMT', 'x-amzn-RequestId': 'd87ee0c8-d004-4199-8974-1727830c9f25', 'x-amzn-ErrorType': 'ForbiddenException', 'x-amz-apigw-id': 'BUPqaFQcDoEF3Xg=', 'X-Cache': 'Error from cloudfront', 'Via': '1.1 5e828cc6ff056cb59ec35c3467ec45f5.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': 'AMS1-C1', 'X-Amz-Cf-Id': '6sbLUxCEmUWQYTWD6BHAommN6eTVieLmL-6XOJh30fNW-IhdcztJIQ=='}
# second call
{'Content-Type': 'application/json', 'Content-Length': '1379', 'Connection': 'keep-alive', 'Date': 'Tue, 22 Jun 2021 07:45:19 GMT', 'x-amzn-RequestId': 'b69a442e-3b3c-4447-bec8-8eff344ac7cb', 'x-amzn-ErrorType': 'IncompleteSignatureException', 'x-amz-apigw-id': 'BUQ6bFLqjoEFjhA=', 'X-Cache': 'Error from cloudfront', 'Via': '1.1 043fc2faaa02eeb59193e3fa300adb6b.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': 'AMS1-C1', 'X-Amz-Cf-Id': '8btFflDUE2I7LxiKbvnmreOE-oKrIwbudgORbWaRrASNUmIPdYtwYQ=='}
My questions are :
Why the first call return a ForbiddenException ? I believe my paths are correctly configured :
Why the second call returns a IncompleteSignatureException ?
PROGRESS
For question 1. Actually, I could find information on AWS documentation page here and was able to understand I simply called an unauthorized method.
For question 2. I was able to diagnose the issue more precisely through the use of API Gateway responses. As shown below, I crafted custom messages for all possible issues
And I was able to find out that the issue was a non-provided API Key. Indeed, my API has a UsagePlan and therefore requires an API Key.
I have modified my script to get this API Key as shown below
session = boto3.Session(profile_name='my-profile')
region = 'eu-west-1' # or 'us-west-1' or whatever
cognito = session.client('cognito-idp', region)
apigw = session.client('apigateway', region)
# Cognito information
cognito_userpool_client_id = 'abcdefghijk27261819'
cognito_username = 'my_username'
cognito_password = 'my_password|'
# API information
apigw_id = 'my_api_id'
def get_api_key(apigw_client, apigw_id):
return apigw_client.get_api_key(apiKey=apigw_id, includeValue=True)['value']
response = cognito.initiate_auth(AuthFlow='USER_PASSWORD_AUTH', AuthParameters={"USERNAME": cognito_username,
"PASSWORD": cognito_password}, ClientId = cognito_userpool_client_id)
# store credentials
accessToken = response['AuthenticationResult']['AccessToken']
idToken = response['AuthenticationResult']['IdToken']
# Call API
HOST = "https://<APIID>.execute-api.eu-west-1.amazonaws.com"
STAGE = "dev"
BASE_URL = f"{HOST}/{STAGE}"
r = requests.post(f"{BASE_URL}/S3Operations/CreateSimulationBucket", headers={"Authorization": idToken, "x-api-key": get_api_key(apigw, apigw_id)})
despite these modifications I still receive the same error :
{'Content-Type': 'application/json', 'Content-Length': '23', 'Connection': 'keep-alive', 'Date': 'Tue, 22 Jun 2021 15:24:27 GMT', 'x-amzn-RequestId': '29743683-dbaf-4d60-bbae-8f2581e1c45a', 'x-amzn-ErrorType': 'ForbiddenException', 'x-amz-apigw-id': 'BVUKxF9FjoEFgbA=', 'issue': 'Invalid API Key', 'X-Cache': 'Error from cloudfront', 'Via': '1.1 bdba42cf1410fb617eeb4ffd3e0b9cb7.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': 'AMS1-C1', 'X-Amz-Cf-Id': 'fMbbQSLlDd7OHErlPCoVNJRZM_P9mq5DYxgsfkJVy4sKHw9lijFYeA=='}

have you deployed the api in api gateway? Have you tried constructing the api call in postman before you do it in python?

Ok. After some analysis, I was able to understand the issue.
For question 1. I could find information on AWS documentation page here and was able to understand I simply called an unauthorized method i.e. calling the root of the API was not authorized because the root had no registered method/ressource.
For question 2. It took me some time to get to the heart of the problem. Indeed, by crafting custom gateway responses, I knew the problem was coming from an Invalid API Key. But this was in fact confusing because the real problem came from an invalid Authorizer !
Prior to solving the issue, here was my SAM template I was using for generating the resources :
API:
Type: AWS::Serverless::Api
Properties:
StageName: !Sub ${Env}
# Authentication on the API will be performed via a Key
Auth:
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt CognitoUserPool.Arn
ApiKeyRequired: true
# this creates a key, a usage plan and associate them
UsagePlan:
CreateUsagePlan: PER_API
UsagePlanName: !Sub ${Project}-${Env}-UsagePlan
So ... I define a Cognito Authorizer ... But I am not actually using it. I was able to identify that in the AWS API Gateway console. Going into the resource page I arrive to the path I want to call and I click on the Method box :
And I arrive at the page shown below. Prior to the correction the Authorization field was None and that was causing my issue
Indeed ... I was making an API call with an Authorization header while there was no Authorization set on the method itself. Now I find it confusing that AWS does not send a more detailed error message and only send a Invalid API key. It took me a while to understand what was causing the issue because of that because I assumed there was an Authorizer on the method.
In order to solve the issue, I had to modify my SAM template as shown below and this modification allowed to set the Cognito Authorizer by default for all methods of the API and allowed me to make the call successfully :
API:
Type: AWS::Serverless::Api
Properties:
StageName: !Sub ${Env}
# Authentication on the API will be performed via a Key
Auth:
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt CognitoUserPool.Arn
DefaultAuthorizer : CognitoAuthorizer
ApiKeyRequired: true
# this creates a key, a usage plan and associate them
UsagePlan:
CreateUsagePlan: PER_API
UsagePlanName: !Sub ${Project}-${Env}-UsagePlan
Notice the addition of the record DefaultAuthorizer : CognitoAuthorizer
Hope this will help others !

Related

How to authenticate Azure REST APIs to Service Bus with SAS

I'm trying to send a message to the Service Bus queue using the REST API provided in this document:
https://learn.microsoft.com/en-us/rest/api/servicebus/send-message-to-queue
Do note that I cannot use the Azure Libraries for this task as there are non available that I know of for Service Now and I'm setting up the test trigger in Python to simulate the REST API calls that would be made from Service Now.
I had a similar question with regards to storage queues and I've tried reusing the same solution however the Service Bus would respond with "Missing Authorization Header"
This is my code that uses the Authorization in the header:
import requests
api = f"https://{service_namespace}.servicebus.windows.net/{queue}/messages?"
msg = """<QueueMessage>
<MessageText>Testing 1234</MessageText>
</QueueMessage>
"""
header = {
"Authorization": f"SharedAccessSignature sr=https://{service_namespace}.servicebus.windows.net/{queue}&sig={sig}&se={se}&skn={skn}",
"Content-Type": "application/atom+xml;type=entry;charset=utf-8"
}
resp = requests.post(api, data=msg, headers=header)
print(resp)
print(resp.text)
print(resp.headers)
Here, sig is the primary key I got from the Service Bus's Queue under Shared Access Policy
se is the epoch time 2 years from now (w/o mili seconds)
skn is the name of the policy
The final response I get is
<Response [401]>
{'Content-Length': '0', 'Server': 'Microsoft-HTTPAPI/2.0', 'Strict-Transport-Security': 'max-age=31536000', 'Date': 'Thu, 24 Feb 2022 09:27:17 GMT'}
If I post without the Auth in the header and use the solution in the highlighted question above,
This is the API structure BTW:
f"https://{service_namespace}.servicebus.windows.net/{queue}/messages?sig={sig}&se={se}&skn={skn}"
I get this error:
<Error><Code>401</Code><Detail>MissingToken: The authorization header was not found. To know more visit https://aka.ms/sbResourceMgrExceptions. . TrackingId:<redacted>, SystemTracker:<redacted>.servicebus.windows.net:<redacted>/messages, Timestamp:2022-02-24T09:31:09</Detail></Error>
{'Transfer-Encoding': 'chunked', 'Content-Type': 'application/xml; charset=utf-8', 'Server': 'Microsoft-HTTPAPI/2.0', 'Strict-Transport-Security': 'max-age=31536000', 'Date': 'Thu, 24 Feb 2022 09:31:09 GMT'}
I am not sure how to proceed with this, any tips and suggestion would be much appreciated.
The reason you are getting this error is because you are computing the shared access signature incorrectly. You can learn more about it here.
To generate SAS token using python, please see the code below which is taken from here:
import time
import urllib
import hmac
import hashlib
import base64
def get_auth_token(sb_name, eh_name, sas_name, sas_value):
"""
Returns an authorization token dictionary
for making calls to Event Hubs REST API.
"""
uri = urllib.parse.quote_plus("https://{}.servicebus.windows.net/{}" \
.format(sb_name, eh_name))
sas = sas_value.encode('utf-8')
expiry = str(int(time.time() + 10000))
string_to_sign = (uri + '\n' + expiry).encode('utf-8')
signed_hmac_sha256 = hmac.HMAC(sas, string_to_sign, hashlib.sha256)
signature = urllib.parse.quote(base64.b64encode(signed_hmac_sha256.digest()))
return {"sb_name": sb_name,
"eh_name": eh_name,
"token":'SharedAccessSignature sr={}&sig={}&se={}&skn={}' \
.format(uri, signature, expiry, sas_name)
}

Suppress printing of response from Azure Queue Storage

When I send a message to a queue I want the response returned into an object which I can then include in my log or not. However for some reason when I execute the following code:
from azure.storage.queue import QueueClient, TextBase64EncodePolicy
# ... some code running ##########################
queue = QueueClient.from_connection_string(conn_str=conn_queue, queue_name="items",
message_encode_policy=TextBase64EncodePolicy())
# ... some message generated #####################
response=queue.send_message(json.dumps(item_dict))
a complete message is printed to my log. It looks for example like this:
Request URL: 'https://{some_storage_account}.queue.core.windows.net/items/messages'
Request method: 'POST'
Request headers: 'Accept': 'application/xml'
'Content-Type': 'application/xml; charset=utf-8'
'x-ms-version': 'REDACTED'
'Content-Length': '1295'
'x-ms-date': 'REDACTED'
'x-ms-client-request-id': '3452464c-06b2-11eb-9f96-00155d6ebdc5'
'User-Agent': 'azsdk-python-storage-queue/12.1.3 Python/3.8.5 (Linux-4.19.104-microsoft-standard-x86_64-with-glibc2.2.5)'
'Authorization': 'REDACTED'
A body is sent with the request
Response status: 201
Response headers:
'Transfer-Encoding': 'chunked'
'Content-Type': 'application/xml'
'Server': 'Windows-Azure-Queue/1.0 Microsoft-HTTPAPI/2.0'
'x-ms-request-id': '1720a5da-c003-002b-18be-9ab4d4000000'
'x-ms-version': 'REDACTED'
'Date': 'Mon, 05 Oct 2020 02:26:41 GMT'
How can I prevent this gulp of information to be printed to my log?
I have spent at least half an hour on the docs from microsoft. But I can't find where I can turn this behaviour off.
logger = logging.getLogger("azure.core.pipeline.policies.http_logging_policy")
logger.setLevel(logging.WARNING)
This Worked For Me - Add this to your code
I tried with the logging-levels - it does not do the trick for me. However when I define a new logger for this connection then it works:
queue_logger = logging.getLogger("logger_name")
# queue_logger.disabled = True
queue_logger.setLevel(logging.DEBUG)
queue = QueueClient.from_connection_string(conn_str=conn_queue, queue_name="debug",message_encode_policy=TextBase64EncodePolicy(), logger=queue_logger)
Ultimately I feel like I should use the REST API instead of the Python Azure SDK for this. REST API allows me to log-output based on the response status. For some weird reason the SDK does not offer me this possibility.
The answer should be here, in the README of azure-storage-python in GitHub:
Here is how we use the logging levels, it is recommended to use INFO:
DEBUG: log strings to sign
INFO: log outgoing requests and responses, as well as retry attempts # <--
WARNING: not used
ERROR: log calls that still failed after all the retries
This should do it, as recommended by Kalies LAMIRI (I didn't try myself):
logger = logging.getLogger("azure.storage")
logger.setLevel(logging.ERROR)

Trigger DAG from google cloud function

I have been trying to execute a DAG in Cloud Composer (composer-1.8.1-airflow-1.10.3) when a file arrives at a GCS bucket based on the cloud composer documentation with Cloud Function in Python
I haven't been able to make it work, every time it triggers, the function returns this message to the log
Exception: Bad response from application: 400 / {'Date': 'Mon, 30 Dec 2019 18:07:02 GMT', 'Content-Type': 'text/html', 'Content-Length': '192', 'Server': 'gunicorn/19.9.0', 'Via': '1.1 google', 'Alt-Svc': 'quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000'} / '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>400 Bad Request</title>\n<h1>Bad Request</h1>\n<p>The browser (or proxy) sent a request that this server could not understand.</p>\n'
The request URL sĀ”generate and the open id token seems correct
The error is raised in the last part of the code
resp = requests.request(
method, url,
headers={'Authorization': 'Bearer {}'.format(
google_open_id_connect_token)}, **kwargs)
if resp.status_code == 403:
raise Exception('Service account {} does not have permission to '
'access the IAP-protected application.'.format(
signer_email))
elif resp.status_code != 200:
raise Exception(
'Bad response from application: {!r} / {!r} / {!r}'.format(
resp.status_code, resp.headers, resp.text))
Thanks
The error code 400 Bad Request reflected in the initial question more often corresponds to the client side problem, indeed the input parameters for DAG triggering should be inspected as a first, the function that invokes IAP call in the code example:
def make_iap_request(url, client_id, method='GET', **kwargs)
Ensure, that you have applied dependencies required to run against IAP protected URL via requirements.txt file.
Do not forget to verify the essential arguments that you pass to this function:
url: The Identity-Aware Proxy-protected URL to fetch.
client_id: The client ID used by Identity-Aware Proxy.
So far I haven't found even better hand on experience about triggering the DAG then explained by #Jacob Ferriero in his great article.

How to use OAuth Applciation token generated by Docusign Python client [ApiClient.request_jwt_application_token()]?

I am writing a back end rest application using python that creates envelopes from templates and returns the front end application the url to the created envelope to be displayed.
Using the Python client, I am able to create a valid OAuth token using the following lines:
api_client = ApiClient("https://demo.docusign.net/restapi")
resp = api_client.request_jwt_application_token(
client_id=client_id, # the integrator key
oauth_host_name=auth_server, # 'account-d.docusign.com'
private_key_bytes=private_key_file.read(), # private key temp file containing key in bytes
expires_in=expiration # 3600
)
The call is successful, and returns the following:
resp
{
'access_token': 'eyJ0eXAi...81z9D5w',
'data': None,
'expires_in': '28800',
'refresh_token': None,
'scope': None,
'token_type': 'Application'
}
And the proper Authorization headers are set in the client:
api_client.default_headers
{
'X-DocuSign-SDK': 'Python',
'User-Agent': 'Swagger-Codegen/1.0.0/python',
'Authorization': 'Application eyJ0eXAi...81z9D5w'
}
However, whenever I try to make any call, I get the following response:
envelope_api = EnvelopeApi(api_client)
envelope_api.get_envelope(account_id, envelope_id)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...
docusign_esign.client.api_exception.ApiException: (400)
Reason: Bad Request
HTTP response headers: HTTPHeaderDict({'Cache-Control': 'no-cache', 'Content-Length': '120', 'Content-Type': 'application/json; charset=utf-8', 'X-DocuSign-TraceToken': '14ac...388', 'Date': 'Fri, 20 Dec 2019 17:47:43 GMT', 'Vary': 'Accept-Encoding'})
HTTP response body: b'{"errorCode":"USER_AUTHENTICATION_FAILED","message":"One or both of Username and Password are invalid. invalid_request"}'
Is there an additional step to utilize this Application token? The Python JWT example is severely out of date, and the Python client repository does not mention any required changes to use the new call
Commit introducing new application token functionality
You're generating an application token which is only useful in specific circumstances like with use with Datafeeds. In general, you want to be generating a user token, which will include a UserId as one of the parameters.
To do so, you need the api_client.configure_jwt_authorization_flow method. An example of how to use that is available on GitHub: https://github.com/docusign/eg-01-python-jwt/blob/master/example_base.py

Where is the access token in the URL that Facebook returns?

I made a request to authenticate server side into Facebook for my Django application.
def authenticateViaFacebook(request):
'''
Redirects users to a page that allows for Facebook login.
'''
consumer = oauth2.Consumer(
key = settings.FACEBOOK_APP_ID,
secret = settings.FACEBOOK_APP_SECRET)
# Request token URL for Facebook.
request_token_url = "https://www.facebook.com/dialog/oauth/"
# Create client.
client = oauth2.Client(consumer)
# Other arguments to Facebook.
otherArguments = 'client_id=' + settings.FACEBOOK_APP_ID
otherArguments += '&scope=email'
otherArguments += '&redirect_uri=' + settings.SITE_URL
otherArguments += 'authenticationRedirect/facebook'
otherArguments += '&secret=' + settings.FACEBOOK_APP_SECRET
# The OAuth Client request works just like httplib2 for the most part.
res, content = client.request(request_token_url, "GET", otherArguments)
# Examine the response.
return HttpResponse(str(res))
I got this response from Facebook.
{'status': '200', 'content-length': '15753', 'x-xss-protection': '0',
'content-location': u'https://www.facebook.com/dialog/oauth/?oauth_body_hash=1YZMc0vtMcUa5nO81ZkuwoCSct4%3D&oauth_nonce=14826580&oauth_timestamp=1342669603&oauth_consumer_key=117889941688718&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_signature=t2cIAmQYs4r5IqcsoODqorFIHbs%3D',
'x-content-type-options': 'nosniff', 'transfer-encoding': 'chunked',
'expires': 'Sat, 01 Jan 2000 00:00:00 GMT', 'connection': 'keep-alive',
'-content-encoding': 'gzip', 'pragma': 'no-cache',
'cache-control': 'private, no-cache, no-store, must-revalidate',
'date': 'Thu, 19 Jul 2012 03:46:44 GMT', 'x-frame-options': 'DENY',
'content-type': 'text/html; charset=utf-8',
'x-fb-debug': 'cXSDiq5jL9ZffjalabM6QKEgr50QOhPJsNlHW2MpSSQ='}
I am a bit confused because I do not see a variable called "access token" I even see weird terms like auth_signature_method and oauth_signature, but I have no idea what they mean.
How do I retrieve the access token from Facebook? Furthermore, how can I use this access token to retrieve basic information such as "name" from the user? The access token is just a string, right?
I don't know with your code. But if you use Facebook graph api it would be simple.
def authorize(self):
warnings.filterwarnings('ignore', category=DeprecationWarning)
savout = os.dup(1)
os.close(1)
os.open(os.devnull, os.O_RDWR)
try:
webbrowser.open(FBOAuth.FACEBOOK_GRAPH_URL+'/oauth/authorize?'+urllib.urlencode(
{'client_id':FBOAuth.CLIENT_ID,
'redirect_uri':FBOAuth.REDIRECT_URI,
'scope':'read_stream, publish_stream'}))
finally:
os.dup2(savout, 1)
FBOAuth.SECRET_CODE = raw_input("Secret Code: ")
self.save_secret_code(FBOAuth.SECRET_CODE)
return FBOAuth.SECRET_CODE
def access_token(self):
if not FBOAuth.SECRET_CODE:
FBOAuth.SECRET_CODE = self.authorize()
args = {'redirect_uri': FBOAuth.REDIRECT_URI,
'client_id' : FBOAuth.CLIENT_ID,
'client_secret':FBOAuth.CLIENT_SECRET,
'code':FBOAuth.SECRET_CODE,}
access_token = urllib.urlopen(FBOAuth.FACEBOOK_GRAPH_URL + "/oauth/access_token?" + urllib.urlencode(args)).read()
access_token = urlparse.parse_qs(access_token)
FBOAuth.ACCESS_TOKEN = access_token['access_token'][0]
self.save_access_token(FBOAuth.ACCESS_TOKEN)
return FBOAuth.ACCESS_TOKEN
And by these 2 functions you can get access tokens.
Here is the link for tutorial i used.
Facebook graph api documents for reference.
Just like most auth processes, you pass in your app ID, and get back a "code." In a second "GET", you send that in for the access_token. You need to decrypt the result, which may be in HMAC. Use the SDK to manage these functions, as the other person recommended, and it will be easier.
"auth_signature_method" means the way in which the encrypted signature was signed. It's not the final access_token you can use to query the user's data on Facebook.
Your code is wrong in several ways, mainly in forgetting that you need to have a user participate in the transaction.
See https://developers.facebook.com/docs/authentication/server-side/ for the the steps to follow, these are actually pretty simple and you can do them all by hand using curl.

Categories