Authorization header is not sent in request using FastAPI - python

In my code, I have a request header "Authorization", but in /docs, the header is not sent:
#router.get('/versions',tags=["Credentials"],responses={
200: {
"model": List[models.versions_info],
"description": "Return has code",
"headers": {"Authorization": {"description":"Token party","type":"string"}}
}})
async def list_versions(request: Request,token: Union[str, None] = Header(alias="Authorization",default=None)):
print(token)
out=[{"version": "2.1.1","url": "https://www.server.com/ocpi/2.1.1/"},{"version": "2.2","url": "https://www.server.com/ocpi/2.2/"}]
return Response(status_code=200,content=json.dumps(out), media_type="application/json", headers={"Authorization": "Token "+config.globals['mytoken']})
In Docs:

You can not do it that way. Authorization is a reserved header here and you can not override it. if you rename it to something different you'll see it in curl and it would work as expected. See here how you can implement jwt-based auth: https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/
Or you can use Security functionality to archive it. This is a self-contained working example (creds to #Pavel Gribov):
from fastapi import Security, Depends, FastAPI
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel
app = FastAPI()
token_key = APIKeyHeader(name="Authorization")
class Token(BaseModel):
token: str
def get_current_token(auth_key: str = Security(token_key)):
return auth_key
#app.get("/get_items")
def read_items(current_token: Token = Depends(get_current_token)):
return current_token
See how it works.

Related

FastAPI with redirects on AWS Lambda : Too many redirects

I have a Spotify project needing authorization codes through their API. I built an API to redirect the user to Spotify's login and then turn back to my API along with the user's code.
The API:
import boto3
import requests
import base64
from fastapi import APIRouter
from fastapi.responses import RedirectResponse
from mangum import Mangum
from client import client_id, client_secret
app = APIRouter()
lambda_handler = Mangum(app,lifespan='off')
url = 'https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.lambda-url.me-south-1.on.aws'
#app.get("/")
async def root():
return RedirectResponse("/login/")
#app.get("/home/")
async def main(code: str):
encoded = base64.b64encode(
(client_id + ":" + client_secret).encode("ascii")
).decode("ascii")
base = "https://accounts.spotify.com/api/token"
payload = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": f"{url}/home/",
}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic " + encoded,
}
tokens = requests.post(base, data=payload, headers=headers).json()
refresh_token = tokens["refresh_token"]
access_token = tokens["access_token"]
email_base_url = "https://api.spotify.com/v1/me"
email_headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
}
email = requests.get(email_base_url, headers=email_headers).json()["email"]
dynamo = boto3.resource("dynamodb")
tokens = dynamo.Table("tokens")
item = {"email": email, "token": access_token, "refresh_token": refresh_token}
tokens.put_item(Item=item)
return {"message": "success"}
#app.get("/login/")
async def login():
base = "https://accounts.spotify.com/authorize?"
base += "response_type=code"
base += f"&client_id={client_id}"
base += "&scope=user-read-recently-played user-read-email"
base += f"&redirect_uri={url}/home/"
return RedirectResponse(base)
The API works as intended when I run it on localhost or use an ngrok tunnel. However, when I upload it to AWS Lambda and generate a function URL, the browser returns the error ERR_TOO_MANY_REDIRECTS for any of the three endpoints.
I've seen questions about this topic, but they all included CloudFront. I would be happy to provide information about the Lambda function as necessary.
Does it makes a different if you call baseurl/ vs baseurl/login ?
If someone is already logged in and hits the baseurl you redirecting him.
/ -> /login -> spotify -> /home
You can try sort / -> /login into one as its still just a redirection.
Update 1:
Do not use the same variable base in login and main.
Log the urls you are redirecting. This will help you understand what is going on.
Urlencode the url before passing it as url param. base += f"&redirect_uri= {urlencode(url)}/home/"

FastApi - api key as parameter secure enough

i am new in this part of programming and i have few questions. First of all my project. At one side i have a Flutter App and at the other side a MS SQL Server with data. This data i need on my device logically. I read the best way is to use FastAPI, its easy and has a good performance but i am not sure about security. I read something about OAuth2 but it looks to much because just one user will have permission to use the data (the server owner). Is it possible just to use a simple api key as a parameter? Something like this...
from fastapi import FastAPI
from SqlServerRequest import SqlServerRequest
app = FastAPI()
#app.get("/openOrders/{key}")
async def openOrders(key):
if key == "myverysecurekey":
return "SQLDATA"
else
return "Wrong key"
That way works but i am not sure about the security
What would you say?
I have been dealing with the same issue for a while. Instead of using a oauth I needed a simple X-API-Key in the header.
You can do that with the following code
from fastapi import FastAPI, Depends
from fastapi.security import APIKeyHeader
import os
os.environ['API-KEY'] = '1234'.
# You would use as an environment var in real life
X_API_KEY = APIKeyHeader(name='X-API-Key')
def api_key_auth(x_api_key: str = Depends(X_API_KEY)):
""" takes the X-API-Key header and validate it with the X-API-Key in the database/environment"""
if x_api_key != os.environ['API-KEY']:
raise HTTPException(
status_code=401,
detail="Invalid API Key. Check that you are passing a 'X-API-Key' on your header."
)
app = FastAPI()
#app.get("/do_something", dependencies=[Depends(api_key_auth)])
async def do_something():
return "API is working OK."
If your use case is just to serve a single user, and is not mission-critical, this might be a good way to start.
main.py
import os
import uvicorn
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from starlette import status
# Use token based authentication
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Ensure the request is authenticated
def auth_request(token: str = Depends(oauth2_scheme)) -> bool:
authenticated = token == os.getenv("API_KEY", "DUMMY-API-KEY")
return authenticated
app = FastAPI()
#app.get("/openOrders")
async def open_orders(authenticated: bool = Depends(auth_request)):
# Check for authentication like so
if not authenticated:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated")
# Business logic here
return {"message": "Authentication Successful"}
if __name__ == '__main__':
uvicorn.run("main:app", host="127.0.0.1", port=8080)
You can run this using python main.py
The client can then make requests like so:
import requests
url = "http://127.0.0.1:8080/openOrders"
payload={}
# The client would pass the API-KEY in the headers
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer DUMMY-API-KEY'
}
response = requests.request("GET", url, headers=headers, data=payload)
print(response.text)
Client code in Dart
final response = await http.get(
Uri.parse('http://127.0.0.1:8080/openOrders'),
// Send authorization headers to the backend.
headers: {
HttpHeaders.authorizationHeader: 'Bearer DUMMY-API-KEY',
},
);

Python Authlib : How To Resolve Auth Code Challenge And Verify Tokens Stored In HTTP Only Session Cookie Of Protected Endpoint?

After reading the documentation I am struggling to understand how to use Authlib to implement Authorize Code Flow for an OpenID Connect provider. After reading the documentation I have had a go at implementing the following code listed below.
The /login endpoint uses authlib to redirect to authorization of Identity Provider, in this case Cognito. This redirects to /aws_cognito_redirect which I have currently implemented myself to resolve the code challenge to retrieve tokens.
My questions are:
How to use authlib to also resolve the code challenge instead of implementing this part myself?
Does Authlib provide functionality to return token(s) in HTTP Only cookie and verify tokens in subsequent requests containing the cookie? For example, does Authlib allow an endpoint to be decorated/marked to as protected, in which case it will verify the tokens in HTTP Only cookie?
Update
After inspecting the source code I eventually figured out how to resolve the code challenge using Authlib with FastAPI. The source code is included at the end of this question.
I am leaving the question open since the second part remains unanswered.
Currently, this question suggests that it is possible to use ResourceProtector class that would do what I need. However, that has a parse_request_authorization method that inspects the Authorisation header of a request for a bearer token. So...I am assuming the approach is to subclass ResourceProtector class and override this method to inspect request for HTTP only cookie and extract the JWT contained within for verification?? Is this feature implemented and provided by Authlib?
Alternatively, also investigating to see if I can integrate fastapi-login to achieve this functionality.
Appendix: Source Code
Initial Source Code With Custom Implementation For Resolving Code Challenge
import base64
from functools import lru_cache
import httpx
from authlib.integrations.starlette_client import OAuth
from fastapi import Depends, FastAPI, Request, Response
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from . import config
#lru_cache()
def get_settings() -> config.Settings:
"""Create config settings instance encapsulating app config."""
return config.Settings()
def get_auth_base_url(region: str, userpool_id: str) -> str:
# base_url = "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_QqNgzdtT5"
base_url = "https://cognito-idp." + region + ".amazonaws.com/" + userpool_id
return base_url
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="secretly")
oauth = OAuth()
oauth.register(
"cognito",
client_id=get_settings().client_id,
client_secret=get_settings().client_secret,
server_metadata_url=get_auth_base_url(
get_settings().region, get_settings().userpool_id
)
+ "/.well-known/openid-configuration",
client_kwargs={"scope": "openid email"},
)
def encode_auth_header(client_id: str, client_secret: str) -> str:
"""Encode client id and secret as base64 client_id:client_secret."""
secret = base64.b64encode(
bytes(client_id, "utf-8") + b":" + bytes(client_secret, "utf-8")
)
return "Basic " + secret.decode()
#app.get("/login")
async def login(request: Request):
"""Redirect to /aws_cognito_redirect endpoint."""
cognito = oauth.create_client("cognito")
redirect_uri = request.url_for("read_code_challenge")
return await cognito.authorize_redirect(request, redirect_uri)
#app.get("/aws_cognito_redirect")
async def read_code_challenge(
request: Request,
response: Response,
settings: config.Settings = Depends(get_settings),
):
"""Retrieve tokens from oauth2/token endpoint and return session cookie."""
code = request.query_params["code"]
print("/aws_cognito_redirect received code := ", code)
auth_secret = encode_auth_header(settings.client_id, settings.client_secret)
headers = {"Authorization": auth_secret}
print("Authorization:" + str(headers["Authorization"]))
payload = {
"client_id": settings.client_id,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": settings.redirect_uri,
}
token_url = (
"https://"
+ settings.domain
+ ".auth."
+ settings.region
+ ".amazoncognito.com/oauth2/token"
)
async with httpx.AsyncClient() as client:
tokens = await client.post(
token_url,
data=payload,
headers=headers,
)
tokens.raise_for_status()
print("Tokens\n" + str(tokens.json()))
response.set_cookie(key="jwt", value=tokens.content, httponly=True)
Updated Source Code To Demonstrate How To Resolve Code Challenge Using Authlib
import base64
from functools import lru_cache
from authlib.integrations.starlette_client import OAuth
from fastapi import FastAPI, Request
from starlette.middleware.sessions import SessionMiddleware
from . import config
#lru_cache()
def get_settings() -> config.Settings:
"""Create config settings instance encapsulating app config."""
return config.Settings()
#lru_cache
def get_auth_base_url(region: str, userpool_id: str) -> str:
"""Return cognito discover points base url from region and userpool ID."""
return ("https://cognito-idp." + region + ".amazonaws.com/" + userpool_id)
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="some-random-string")
oauth = OAuth()
cognito = oauth.register(
"cognito",
client_id=get_settings().client_id,
client_secret=get_settings().client_secret,
server_metadata_url=get_auth_base_url(
get_settings().region, get_settings().userpool_id
)
+ "/.well-known/openid-configuration",
client_kwargs={"scope": "openid email"},
)
def encode_auth_header(client_id: str, client_secret: str) -> str:
"""Encode client id and secret as base64 client_id:client_secret."""
secret = base64.b64encode(
bytes(client_id, "utf-8") + b":" + bytes(client_secret, "utf-8")
)
return "Basic " + secret.decode()
#app.get("/")
async def login(request: Request):
"""Redirect to /aws_cognito_redirect endpoint after sign-in."""
redirect_uri = request.url_for("read_code_challenge")
return await cognito.authorize_redirect(request, redirect_uri)
#app.get("/aws_cognito_redirect")
async def read_code_challenge(request: Request):
"""Request a token from cognito using code challenge response."""
return await cognito.authorize_access_token(request)

requests-mock: how can I match POSTed payload in a mocked endpoint

What I've Done
I've written an authentication class for obtaining an application's bearer token from Twitter using the application's API Key and its API key secret as demonstrated in the Twitter developer docs.
I've mocked the appropriate endpoint using requests_mock this way:
#pytest.fixture
def mock_post_bearer_token_endpoint(
requests_mock, basic_auth_string, bearer_token
):
requests_mock.post(
"https://api.twitter.com/oauth2/token",
request_headers={
"Authorization": f"Basic {basic_auth_string}",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
json={"token_type": "bearer", "access_token": f"{bearer_token}"},
)
And my test method is :
#pytest.mark.usefixtures("mock_post_bearer_token_endpoint")
def test_basic_auth(api_key, api_key_secret, bearer_token):
response = requests.post(
'https://api.twitter.com/oauth2/token',
data={"grant_type": "client_credentials"},
auth=TwitterBasicAuth(api_key, api_key_secret),
)
assert response.json()['access_token'] == bearer_token
(Where TwitterBasicAuth is the authentication class I wrote, and the fixture basic_auth_string is a hardcoded string that would be obtained from transforming the fixtures api_key and api_key_secret appropriately).
And it works.
The Problem
But I'm really bothered by the fact that the mocked endpoint doesn't check the payload. In this particular case, the payload is vital to obtain a bearer token.
I've combed through the documentation for requests_mock (and responses, too) but haven't figured out how to make the endpoint respond with a bearer token only when the correct payload is POSTed.
Please help.
I think the misconception here is that you need to put everything in the matcher and let NoMatchException be the thing to tell you if you got it right.
The matcher can be the simplest thing it needs to be in order to return the right response and then you can do all the request/response checking as part of your normal unit test handling.
additional_matchers is useful if you need to switch the response value based on the body of the request for example, and typically true/false is sufficient there.
eg, and i made no attempt to look up twitter auth for this:
import requests
import requests_mock
class TwitterBasicAuth(requests.auth.AuthBase):
def __init__(self, api_key, api_key_secret):
self.api_key = api_key
self.api_key_secret = api_key_secret
def __call__(self, r):
r.headers['x-api-key'] = self.api_key
r.headers['x-api-key-secret'] = self.api_key_secret
return r
with requests_mock.mock() as m:
api_key = 'test'
api_key_secret = 'val'
m.post(
"https://api.twitter.com/oauth2/token",
json={"token_type": "bearer", "access_token": "token"},
)
response = requests.post(
'https://api.twitter.com/oauth2/token',
data={"grant_type": "client_credentials"},
auth=TwitterBasicAuth(api_key, api_key_secret),
)
assert response.json()['token_type'] == "bearer"
assert response.json()['access_token'] == "token"
assert m.last_request.headers['x-api-key'] == api_key
assert m.last_request.headers['x-api-key-secret'] == api_key_secret
https://requests-mock.readthedocs.io/en/latest/history.html
Updated Answer
I went with gold_cy's comment and wrote a custom matcher that takes a request and returns an appropriately crafted OK response if the request has the correct url path, headers and json payload. It returns a 403 response otherwise, as I'd expect from the Twitter API.
#pytest.fixture
def mock_post_bearer_token_endpoint(
requests_mock, basic_auth_string, bearer_token
):
def matcher(req):
if req.path != "/oauth2/token":
# no mock address
return None
if req.headers.get("Authorization") != f"Basic {basic_auth_string}":
return create_forbidden_response()
if (
req.headers.get("Content-Type")
!= "application/x-www-form-urlencoded;charset=UTF-8"
):
return create_forbidden_response()
if req.json().get("grant_type") != "client_credentials":
return create_forbidden_response()
resp = requests.Response()
resp._content = json.dumps(
{"token_type": "bearer", "access_token": f"{bearer_token}"}
).encode()
resp.status_code = 200
return resp
requests_mock._adapter.add_matcher(matcher)
yield
def create_forbidden_response():
resp = requests.Response()
resp.status_code = 403
return resp
Older Answer
I went with gold_cy's comment and wrote an additional matcher that takes the request and checks for the presence of the data of interest in the payload.
#pytest.fixture(name="mock_post_bearer_token_endpoint")
def fixture_mock_post_bearer_token_endpoint(
requests_mock, basic_auth_string, bearer_token
):
def match_grant_type_in_payload(request):
if request.json().get("grant_type") == "client_credentials":
return True
resp = Response()
resp.status_code = 403
resp.raise_for_status()
requests_mock.post(
"https://api.twitter.com/oauth2/token",
request_headers={
"Authorization": f"Basic {basic_auth_string}",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
json={"token_type": "bearer", "access_token": f"{bearer_token}"},
additional_matcher=match_grant_type_in_payload,
)
I opted to raise an Http403 error (instead of just returning False) in order to reduce the cognitive load of determining the reason exceptions are raised — returning False would lead to a requests_mock.exceptions.NoMockAddress being raised, which I don't think is descriptive enough in this case.
I still think there's a better way around this, and I'll keep searching for it.

How to call an AppSync mutation with Cognito authentication using python?

Is it possible to calling an AppSync mutation with Cognito authentication using Python? How?
I am trying to use boto3, but I don't found a way to execute graphql operations.
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appsync.html
You can turn your API auth mode to be "API KEY" and call an AppSync mutation with http.
For example.
import requests
import json
APPSYNC_API_KEY = 'da2-xxxxxxxxxxxxx'
APPSYNC_API_ENDPOINT_URL = 'https://xxxxxxxxxxxxx.appsync-api.us-west-2.amazonaws.com/graphql'
headers = {
'Content-Type': "application/graphql",
'x-api-key': APPSYNC_API_KEY,
'cache-control': "no-cache",
}
def execute_gql(query):
payload_obj = {"query": query}
payload = json.dumps(payload_obj)
response = requests.request("POST", APPSYNC_API_ENDPOINT_URL, data=payload, headers=headers)
return response
Imagine you have a model called Items and you can easily make query like below:
if __name__ == '__main__':
print(execute_gql("query { listItems { items { id name } } }").json())
Simply replace the string with the mutation operation.
It appears that idToken from auth response is the only one you need to pass under the Authorization header. Nothing more is required. Auth middleware shouldn't be passed in the Transport object at all. Here is how it finally worked from my side:
import boto3
from gql import gql, Client as GQLClient
from gql.transport.requests import RequestsHTTPTransport
username = "YOUR_USER_NAME"
password = "YOUR_PASSWORD"
authClientId = "YOUR_COGNITO_AUTH_APP_CLIENT_ID"
regionName = "YOUR_REGION_NAME"
appSyncEndpoint = "YOUR_APPSYNC_ENDPOINT"
def authenticate():
cgClient = boto3.client("cognito-idp", region_name=regionName)
response = cgClient.initiate_auth(
ClientId=authClientId,
AuthFlow="USER_PASSWORD_AUTH",
AuthParameters={"USERNAME": username, "PASSWORD": password},
)
return response["AuthenticationResult"]["IdToken"]
def make_client(idToken):
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': idToken #<<<< Just an idToken
}
transport = RequestsHTTPTransport(url=appSyncEndpoint,
headers=headers) #<<<< there is no `auth` parameter here at all
client = GQLClient(transport=transport,
fetch_schema_from_transport=False)
return client
creds = authenticate()
queryText = """mutation createMessage($message: String!) {
createMessage(input: {message: $message}) {
id
message
createdAt
}
}"""
asClient = make_client(creds)
result = asClient.execute(gql(queryText))
print(result)
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 (for Cognito for example) 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

Resources