Webhook verification fails with Shopify triggering an Azure function - python

I'm trying to create a webhook consumer:
Shopify is posting the data
an Azure function written in Python is triggered by HTTP
the sample code below just logged the result of the webhook verificationthe result of the verification is being logged
There's an example by Shopify, but as I'm not using Flask (like in this SO question), I modified the code a bit:
import azure.functions as func
import logging
import hmac
import hashlib
import base64
import json
app = func.FunctionApp()
#app.function_name(name="webhook-trigger")
#app.route(route="webhook", auth_level=func.AuthLevel.ANONYMOUS)
def webhook_process(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Python HTTP trigger function is processing a request.')
verified = verify(req)
logging.info("Webhook verified: {}".format(verified))
return func.HttpResponse(
"This HTTP triggered function executed successfully.",
status_code=200
)
def verify(req: func.HttpRequest) -> bool:
CLIENT_SECRET = 'xxxxx'
header = req.headers
data = req.get_json()
encoded_data = json.dumps(data).encode('utf-8')
digest = hmac.new(CLIENT_SECRET.encode('utf-8'), encoded_data, digestmod=hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest)
logging.info("computed_hmac: {}".format(computed_hmac))
logging.info("header['X-Shopify-Hmac-Sha256'] encoded: {}".format(header['X-Shopify-Hmac-Sha256'].encode('utf-8')))
verified = hmac.compare_digest(computed_hmac, header['X-Shopify-Hmac-Sha256'].encode('utf-8'))
return verified
The logging output:
I don't understand, why the verification fails (possibly due to some encoding stuff which I did wrong. Any ideas?

Related

Integration testing FastAPI with user authentication

I am trying to write some integration tests for my FastAPI endpoints and am not sure what the best solution is to testing the endpoints that require a user to be logged in.
I am following the FastAPI authentication documentation for my auth flow which is just username password and then receiving a token.
How can I test endpoints using a logged in user?
Example endpoint to test:
#app.get("/lists/{id}", response_model=ListDto)
async def get_list(
id: int,
current_user: User = Depends(get_current_active_user),
):
usecase = GetList(list_id=id, current_user=current_user)
list = usecase()
if not llist:
raise HTTPException(status_code=404, detail=f"List with id:{id} not found")
return list
To test endpoints that require a user to be logged in, you will need to simulate a login process to obtain a valid token and then pass it as a header to your subsequent requests. Here is an example of how you can do this in your integration tests:
Create a fixture for your client and a test user
from fastapi.testclient import TestClient
from app.main import app
#pytest.fixture(scope="module")
def client():
with TestClient(app) as c:
yield c
#pytest.fixture(scope="module")
def test_user():
return {"username": "testuser", "password": "testpass"}
Write a test case for the login endpoint that retrieves a valid token for your test user.
def test_login(client, test_user):
response = client.post("/login", data=test_user)
assert response.status_code == 200
token = response.json()["access_token"]
assert token is not None
return token
Pass the token as a header in subsequent requests that require authentication. For example, here is how you can test the get_list endpoint:
def test_get_list(client, test_user):
token = test_login(client, test_user)
response = client.get("/lists/1", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 200
assert response.json()["id"] == 1

AWS SES - Listing email identities with AWS Lambda

I'm sending emails to verified identities in AWS SES with ASW Lambda without problems.
Now I'm just trying to list verified identities and getting no response.
Here is my code:
import boto3
from botocore.exceptions import ClientError
def list_identities():
ses = boto3.client('ses')
response = ses.list_identities(
IdentityType = 'EmailAddress',
MaxItems=10
)
def lambda_handler(event, context):
# TODO implement
print("Listing EMAILS:")
list_identities()
In function log I see printed Listing email: and nothing else.
Lambda function is invoked in same region as AWS SES.
You don't return anything from your function.
Try this:
import boto3
def list_identities():
ses = boto3.client('ses')
response = ses.list_identities(
IdentityType='EmailAddress',
MaxItems=10
)
return response
def lambda_handler(event, context):
# TODO implement
print("Listing EMAILS:")
print(list_identities())

AuthorizationPermissionMismatch when Python Function App attempts to read file

When I try to read a txt-file from blob storage using a Function App, it returns this error in the log:
Result: Failure Exception: HttpResponseError: This request is not authorized to perform this operation using this permission. RequestId:00000-0000-00000-00000-000000000000 Time:2021-07-28T13:14:46.7803762Z ErrorCode:AuthorizationPermissionMismatch Error:None
In the Access Control menu of the storage account, the role 'Storage Blob Data Contributor' has been given to the system-assigned-identity of the Function App.
This is my code:
import logging
import azure.functions as func
from azure.storage.blob import BlobServiceClient, BlobClient
from azure.identity import DefaultAzureCredential
def main(req: func.HttpRequest) -> func.HttpResponse:
blob_url = "https://my-storage-account.blob.core.windows.net"
blob_credential = DefaultAzureCredential()
blob_client = BlobClient(account_url=blob_url, container_name='tests', blob_name='file.txt', credential=blob_credential)
download_stream = blob_client.download_blob()
logging.info('Contents of the download_stream: %s', download_stream)
return func.HttpResponse("OK", status_code=200)
Why do I get the error instead of the contents of the 'file.txt'?
The system-assigned-identity also needs the role 'Storage Queue Data Contributor
'. And to show the contents of the file in the logging 'download_stream' should be replaced by download_stream.readall().

How to use HTTP Basic Auth as separate FastAPI service?

What I want to achieve? Have one service responsible for HTTP Basic Auth (access) and two services (a, b) where some endpoints are protected by access service.
Why? In scenario where there will be much more services with protected endpoints to not duplicate authorize function in each service. Also to do modification in one place in case of changing to OAuth2 (maybe in future).
What I did?
I followed guide on official website and created example service which works totally fine.
Problem occurs when I try to move authorization to separate service and then use it within few other
services with protected endpoints. I can't figure out how to do it. Could you please help me out?
I have tried different functions setup. Nothing helped, so far my code looks like this:
access-service
import os
import secrets
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def authorize(credentials: HTTPBasicCredentials = Depends(security)):
is_user_ok = secrets.compare_digest(credentials.username, os.getenv('LOGIN'))
is_pass_ok = secrets.compare_digest(credentials.password, os.getenv('PASSWORD'))
if not (is_user_ok and is_pass_ok):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Incorrect email or password.',
headers={'WWW-Authenticate': 'Basic'},
)
app = FastAPI(openapi_url="/api/access/openapi.json", docs_url="/api/access/docs")
#app.get('/api/access/auth', dependencies=[Depends(authorize)])
def auth():
return {"Granted": True}
a-service
import httpx
import os
from fastapi import Depends, FastAPI, HTTPException, status
ACCESS_SERVICE_URL = os.getenv('ACCESS_SERVICE_URL')
app = FastAPI(openapi_url="/api/a/openapi.json", docs_url="/api/a/docs")
def has_access():
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'))
if result.status_code == 401:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Login first.',
)
#app.get('/api/a/unprotected_a')
async def unprotected_a():
return {"Protected": False}
#app.get('/api/a/protected_a', dependencies=[Depends(has_access)])
async def protected_a():
return {"Protected": True}
#app.get('/api/a/protected_b', dependencies=[Depends(has_access)])
async def protected_b():
return {"Protected": True}
The issue here is that, when you are calling Service_A with credentials it's making a call to the Access_Service in the has_access() function.
If you look closely,
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'))
You are simply making a GET call without forwarding the credentials as headers for this request to the Access_Service.
Rewrite your has_access() in all the services to
from typing import Optional
from fastapi import Header
def has_access(authorization: Optional[str] = Header(None)):
if not authorization:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Credentials missing!',
)
headers = {'Authorization': authorization}
result = httpx.get(os.getenv('ACCESS_SERVICE_URL'), headers=headers)
if result.status_code == 401:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='No access to resource. Login first.',
)
Inside your access service you have mistakenly typed True as true,
#app.get('/api/access/auth', dependencies=[Depends(authorize)])
def auth():
return {"Granted": True}
I have cloned your repo and tested it, it's working now. Please check and confirm.
[EDIT] Swagger does not allow authorization header for basic auth (https://github.com/tiangolo/fastapi/issues/612)
Work-Around (not recommended)
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def has_access(credentials: HTTPBasicCredentials = Depends(security), authorization: Optional[str] = Header(None)):
Thanks to Soumojit Ghosh answer and FastAPI Issue 1037 I figured out how should I modify my code. a-service after changes:
import httpx
import os
from fastapi import Depends, FastAPI, Header, HTTPException, status
from typing import Optional
from fastapi.security import HTTPBasicCredentials, HTTPBearer
security = HTTPBearer()
ACCESS_SERVICE_URL = os.getenv('ACCESS_SERVICE_URL')
app = FastAPI(openapi_url="/api/a/openapi.json", docs_url="/api/a/docs")
def has_access(credentials: HTTPBasicCredentials = Depends(security)):
response = httpx.get(os.getenv('ACCESS_SERVICE_URL'), headers={'Authorization': credentials.credentials})
if response.status_code == 401:
raise HTTPException(status_code=401)
#app.get('/api/a/unprotected_a')
async def unprotected_a():
return {"Protected": False}
#app.get('/api/a/protected_a', dependencies=[Depends(has_access)])
async def protected_a():
return {"Protected": True}
#app.get('/api/a/protected_b', dependencies=[Depends(has_access)])
async def protected_b():
return {"Protected": True}
Now header can be sent through SwaggerUI. Click Authorize and then enter it in Value field. To generate your header from login and password you can use for example this tool. It will look like: Basic YWRtaW46cGFzc3dvcmQ=.

aws appsync graphql api with python

I want to access AWS AppSync API using Python code and confused with requests library.
Auth mode is Cognito user pool. My questions are:
How to get access tokens from Cognito user pool?
How to make queries, mutations, and handle subscriptions?
I tried to do it with auth mode API key. but I am getting the following error.
import requests
import json
URL = "https://vtcarmq7zzeadnkwzcgfr24irm.appsync-api.us-east-1.amazonaws.com/graphql"
headers = {"x-api-key":"da2-bwuyzqchhfgyxemcmdinjegb7e"}
data = json.dumps({
"query": '''
listTodos(filter:{
title:{
contains:"g"
}
} ) {
items{
id title duedate
}
}
'''
} )
r = requests.request("POST", URL , data = data , headers = headers)
print(r.text)
{ "errors" : [ {
"message" : "Unable to parse GraphQL query.",
"errorType" : "MalformedHttpRequestException" } ] }
I have seen this video https://www.youtube.com/watch?v=2U4RsbFO4bA&t=1172s
In this video, for authentication using cognito user pool, he says to make a call to cognito user pool and get the tokens and pass it to aws appsync in headers.
I am new to aws and python request module, trying to write python code for this video.
graphql-python/gql supports AWS AppSync since version 3.0.0rc0.
It supports queries, mutation and even subscriptions on the realtime endpoint.
It supports IAM, api key and JWT authentication methods.
The documentation is available here
Here is an example of a mutation using the API Key authentication:
import asyncio
import os
import sys
from urllib.parse import urlparse
from gql import Client, gql
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.appsync_auth import AppSyncApiKeyAuthentication
# Uncomment the following lines to enable debug output
# import logging
# logging.basicConfig(level=logging.DEBUG)
async def main():
# Should look like:
# https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.REGION.amazonaws.com/graphql
url = os.environ.get("AWS_GRAPHQL_API_ENDPOINT")
api_key = os.environ.get("AWS_GRAPHQL_API_KEY")
if url is None or api_key is None:
print("Missing environment variables")
sys.exit()
# Extract host from url
host = str(urlparse(url).netloc)
auth = AppSyncApiKeyAuthentication(host=host, api_key=api_key)
transport = AIOHTTPTransport(url=url, auth=auth)
async with Client(
transport=transport, fetch_schema_from_transport=False,
) as session:
query = gql(
"""
mutation createMessage($message: String!) {
createMessage(input: {message: $message}) {
id
message
createdAt
}
}"""
)
variable_values = {"message": "Hello world!"}
result = await session.execute(query, variable_values=variable_values)
print(result)
asyncio.run(main())

Categories