I am trying to proxy an external website (Flower monitoring URL running on different container) using python Fast API framework:
client = AsyncClient(base_url=f'http://containername:7800/monitor')
#app.get(“/monitor/{path:path}”)
async def tile_request(path: str):
req = client.build_request("GET", path)
r = await client.send(req, stream=True)
return StreamingResponse(
r.aiter_raw(),
background=BackgroundTask(r.aclose),
headers=r.headers
)
It is able to proxy the container URL for every path. For ex.
http://python_server:8001/monitor/dashboard --> http://containername:7800/monitor/dashboard
http://python_server:8001/monitor/tasks --> http://containername:7800/monitor/tasks
It works well. But it fails when the PATH has some query params in the URL.
For ex.
http://python_server:8001/monitor/dashboard?json=1&_=1641485992460 --> redirects to http://containername:7800/monitor/dashboard
(Please note that no query params are appended to the URL).
Can anyone please help with how we can proxy this external website's any path with any query param.
This code works for me and is used in production:
import httpx
from httpx import AsyncClient
from fastapi import Request
from fastapi.responses import StreamingResponse
from starlette.background import BackgroundTask
app = FastAPI()
HTTP_SERVER = AsyncClient(base_url="http://localhost:8000/")
async def _reverse_proxy(request: Request):
url = httpx.URL(path=request.url.path, query=request.url.query.encode("utf-8"))
rp_req = HTTP_SERVER.build_request(
request.method, url, headers=request.headers.raw, content=await request.body()
)
rp_resp = await HTTP_SERVER.send(rp_req, stream=True)
return StreamingResponse(
rp_resp.aiter_raw(),
status_code=rp_resp.status_code,
headers=rp_resp.headers,
background=BackgroundTask(rp_resp.aclose),
)
app.add_route("/{path:path}", _reverse_proxy, ["GET", "POST"])
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',
},
);
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)
Im trying to create a script, either by Python or PHP, that can receive JSON from Dialogflow webhooks.
The data value will be from resolvedQuery.
I want to parse the data then send to the below URL:
https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush?text=***JSON GOES HERE ***&deviceId=18c972b753ad&apikey=5b48aed7
The data from resolvedQuery needs to be sent to the about URL where *JSON GOES HERE * is.
Here is the Python code I have been trying:
from flask import Flask
from flask import request
from flask import make_response
import json
import logging as l
import requests
app = Flask(__name__)
#app.route('/')
def hello():
"""Return a friendly HTTP greeting."""
l.info("reached the hello() module...")
return 'Hello Beautiful World!\n'
#app.route('/apiai', methods=['POST'])
def apiai_response():
requests_session = requests.session()
requests_session.headers.update({'Content-Type': 'application/json'})
requests_session.headers.update({'charset':'utf-8'})
post_data = '[{ "resolvedQuery": "" }]'
requests_response = requests_session.post(url="https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush?text=***JSON GOES HERE ***&deviceId=18c972b753ad&apikey=5b48aed7", data=post_data)
print requests_response.content
#app.errorhandler(404)
def page_not_found(e):
"""Return a custom 404 error."""
return 'Sorry, nothing at this URL.', 404
I would try something like this:
import json
import requests
json_string = json.dumps({"hello": "world"})
these_params = {"text": json_string,
"deviceId": "18c972b753ad&apikey=5b48aed7"}
this_url = "https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush"
r.get(url=this_url, params=these_params)
notice it is a get request and not a post request
It is a matter of opinion however using url encoded params on a GET request differ in how stuff gets read. As far as the data field is concerned on a post request, the handling is a bit different.
On a get request it will look something like part of the url as :
text=%7B%22hello%22%3A%22world%22%7D
Where as a post request the server will try to read in a application/json encoded body which expects the "data" to be valid json text.
also note:
How to urlencode a querystring in Python?
I am trying to make a cross origin request using jquery but it keeps being reject with the message
XMLHttpRequest cannot load http://... No 'Access-Control-Allow-Origin'
header is present on the requested resource. Origin ... is therefore
not allowed access.
I am using flask, heroku, and jquery
the client code looks like this:
$(document).ready(function() {
$('#submit_contact').click(function(e){
e.preventDefault();
$.ajax({
type: 'POST',
url: 'http://...',
// data: [
// { name: "name", value: $('name').val()},
// { name: "email", value: $('email').val() },
// { name: "phone", value: $('phone').val()},
// { name: "description", value: $('desc').val()}
//
// ],
data:"name=3&email=3&phone=3&description=3",
crossDomain:true,
success: function(msg) {
alert(msg);
}
});
});
});
on the heroku side i am using flask and it is like this
from flask import Flask,request
from flask.ext.mandrill import Mandrill
try:
from flask.ext.cors import CORS # The typical way to import flask-cors
except ImportError:
# Path hack allows examples to be run without installation.
import os
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.sys.path.insert(0, parentdir)
from flask.ext.cors import CORS
app = Flask(__name__)
app.config['MANDRILL_API_KEY'] = '...'
app.config['MANDRILL_DEFAULT_FROM']= '...'
app.config['QOLD_SUPPORT_EMAIL']='...'
app.config['CORS_HEADERS'] = 'Content-Type'
mandrill = Mandrill(app)
cors = CORS(app)
#app.route('/email/',methods=['POST'])
def hello_world():
name=request.form['name']
email=request.form['email']
phone=request.form['phone']
description=request.form['description']
mandrill.send_email(
from_email=email,
from_name=name,
to=[{'email': app.config['QOLD_SUPPORT_EMAIL']}],
text="Phone="+phone+"\n\n"+description
)
return '200 OK'
if __name__ == '__main__':
app.run()
Here is what worked for me when I deployed to Heroku.
http://flask-cors.readthedocs.org/en/latest/
Install flask-cors by running -
pip install -U flask-cors
from flask import Flask
from flask_cors import CORS, cross_origin
app = Flask(__name__)
cors = CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'
#app.route("/")
#cross_origin()
def helloWorld():
return "Hello, cross-origin-world!"
I've just faced the same issue and I came to believe that the other answers are a bit more complicated than they need to be, so here's my approach for those who don't want to rely on more libraries or decorators:
A CORS request actually consists of two HTTP requests. A preflight request and then an actual request that is only made if the preflight passes successfully.
The preflight request
Before the actual cross domain POST request, the browser will issue an OPTIONS request. This response should not return any body, but only some reassuring headers telling the browser that it's alright to do this cross-domain request and it's not part of some cross site scripting attack.
I wrote a Python function to build this response using the make_response function from the flask module.
def _build_cors_preflight_response():
response = make_response()
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add("Access-Control-Allow-Headers", "*")
response.headers.add("Access-Control-Allow-Methods", "*")
return response
This response is a wildcard one that works for all requests. If you want the additional security gained by CORS, you have to provide a whitelist of origins, headers and methods.
This response will convince your (Chrome) browser to go ahead and do the actual request.
The actual request
When serving the actual request you have to add one CORS header - otherwise the browser won't return the response to the invoking JavaScript code. Instead the request will fail on the client-side. Example with jsonify
response = jsonify({"order_id": 123, "status": "shipped"})
response.headers.add("Access-Control-Allow-Origin", "*")
return response
I also wrote a function for that.
def _corsify_actual_response(response):
response.headers.add("Access-Control-Allow-Origin", "*")
return response
allowing you to return a one-liner.
Final code
from flask import Flask, request, jsonify, make_response
from models import OrderModel
flask_app = Flask(__name__)
#flask_app.route("/api/orders", methods=["POST", "OPTIONS"])
def api_create_order():
if request.method == "OPTIONS": # CORS preflight
return _build_cors_preflight_response()
elif request.method == "POST": # The actual request following the preflight
order = OrderModel.create(...) # Whatever.
return _corsify_actual_response(jsonify(order.to_dict()))
else:
raise RuntimeError("Weird - don't know how to handle method {}".format(request.method))
def _build_cors_preflight_response():
response = make_response()
response.headers.add("Access-Control-Allow-Origin", "*")
response.headers.add('Access-Control-Allow-Headers', "*")
response.headers.add('Access-Control-Allow-Methods', "*")
return response
def _corsify_actual_response(response):
response.headers.add("Access-Control-Allow-Origin", "*")
return response
OK, I don't think the official snippet mentioned by galuszkak should be used everywhere, we should concern the case that some bug may be triggered during the handler such as hello_world function. Whether the response is correct or uncorrect, the Access-Control-Allow-Origin header is what we should concern. So, it is very simple, just like the snippet bellow:
# define your bluprint
from flask import Blueprint
blueprint = Blueprint('blueprint', __name__)
# put this sippet ahead of all your bluprints
# blueprint can also be app~~
#blueprint.after_request
def after_request(response):
header = response.headers
header['Access-Control-Allow-Origin'] = '*'
# Other headers can be added here if needed
return response
# write your own blueprints with business logics
#blueprint.route('/test', methods=['GET'])
def test():
return "test success"
That is all~~
If you want to enable CORS for all routes, then just install flask_cors extension (pip3 install -U flask_cors) and wrap app like this: CORS(app).
That is enough to do it (I tested this with a POST request to upload an image, and it worked for me):
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # This will enable CORS for all routes
Important note: if there is an error in your route, let us say you try to print a variable that does not exist, you will get a CORS error related message which, in fact, has nothing to do with CORS.
I resolved this same problem in python using flask and with this library.
flask_cors
in file init.py:
#pip install flask_cors
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
cors = CORS(app, resource={
r"/*":{
"origins":"*"
}
})
and its all.
Reference: https://flask-cors.readthedocs.io/en/latest/
Improving the solution described here: https://stackoverflow.com/a/52875875/10299604
With after_request we can handle the CORS response headers avoiding to add extra code to our endpoints:
### CORS section
#app.after_request
def after_request_func(response):
origin = request.headers.get('Origin')
if request.method == 'OPTIONS':
response = make_response()
response.headers.add('Access-Control-Allow-Credentials', 'true')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
response.headers.add('Access-Control-Allow-Headers', 'x-csrf-token')
response.headers.add('Access-Control-Allow-Methods',
'GET, POST, OPTIONS, PUT, PATCH, DELETE')
if origin:
response.headers.add('Access-Control-Allow-Origin', origin)
else:
response.headers.add('Access-Control-Allow-Credentials', 'true')
if origin:
response.headers.add('Access-Control-Allow-Origin', origin)
return response
### end CORS section
All the responses above work okay, but you'll still probably get a CORS error, if the application throws an error you are not handling, like a key-error, if you aren't doing input validation properly, for example. You could add an error handler to catch all instances of exceptions and add CORS response headers in the server response
So define an error handler - errors.py:
from flask import json, make_response, jsonify
from werkzeug.exceptions import HTTPException
# define an error handling function
def init_handler(app):
# catch every type of exception
#app.errorhandler(Exception)
def handle_exception(e):
#loggit()!
# return json response of error
if isinstance(e, HTTPException):
response = e.get_response()
# replace the body with JSON
response.data = json.dumps({
"code": e.code,
"name": e.name,
"description": e.description,
})
else:
# build response
response = make_response(jsonify({"message": 'Something went wrong'}), 500)
# add the CORS header
response.headers['Access-Control-Allow-Origin'] = '*'
response.content_type = "application/json"
return response
then using Billal's answer:
from flask import Flask
from flask_cors import CORS
# import error handling file from where you have defined it
from . import errors
app = Flask(__name__)
CORS(app) # This will enable CORS for all routes
errors.init_handler(app) # initialise error handling
Try the following decorators:
#app.route('/email/',methods=['POST', 'OPTIONS']) #Added 'Options'
#crossdomain(origin='*') #Added
def hello_world():
name=request.form['name']
email=request.form['email']
phone=request.form['phone']
description=request.form['description']
mandrill.send_email(
from_email=email,
from_name=name,
to=[{'email': app.config['QOLD_SUPPORT_EMAIL']}],
text="Phone="+phone+"\n\n"+description
)
return '200 OK'
if __name__ == '__main__':
app.run()
This decorator would be created as follows:
from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper
def crossdomain(origin=None, methods=None, headers=None,
max_age=21600, attach_to_all=True,
automatic_options=True):
if methods is not None:
methods = ', '.join(sorted(x.upper() for x in methods))
if headers is not None and not isinstance(headers, basestring):
headers = ', '.join(x.upper() for x in headers)
if not isinstance(origin, basestring):
origin = ', '.join(origin)
if isinstance(max_age, timedelta):
max_age = max_age.total_seconds()
def get_methods():
if methods is not None:
return methods
options_resp = current_app.make_default_options_response()
return options_resp.headers['allow']
def decorator(f):
def wrapped_function(*args, **kwargs):
if automatic_options and request.method == 'OPTIONS':
resp = current_app.make_default_options_response()
else:
resp = make_response(f(*args, **kwargs))
if not attach_to_all and request.method != 'OPTIONS':
return resp
h = resp.headers
h['Access-Control-Allow-Origin'] = origin
h['Access-Control-Allow-Methods'] = get_methods()
h['Access-Control-Max-Age'] = str(max_age)
if headers is not None:
h['Access-Control-Allow-Headers'] = headers
return resp
f.provide_automatic_options = False
return update_wrapper(wrapped_function, f)
return decorator
You can also check out this package Flask-CORS
You'd first need to install flask-cors. You can do this as follows:
pip install flask-cors
Once this is installed, you can use it in your Flask app as follows:
If you need to enable CORS for all routes:
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
If you want to enable CORS only for specific routes, you can pass the resources parameter to the CORS function.
For example,
CORS(app, resources={r"/api/*": {"origins": "*"}})
In this example, This code will enable CORS only for routes that start with /api/ and will allow requests from any origin. You can customize the resources parameter to match your needs.
For more information, please read the documentation
My solution is a wrapper around app.route:
def corsapp_route(path, origin=('127.0.0.1',), **options):
"""
Flask app alias with cors
:return:
"""
def inner(func):
def wrapper(*args, **kwargs):
if request.method == 'OPTIONS':
response = make_response()
response.headers.add("Access-Control-Allow-Origin", ', '.join(origin))
response.headers.add('Access-Control-Allow-Headers', ', '.join(origin))
response.headers.add('Access-Control-Allow-Methods', ', '.join(origin))
return response
else:
result = func(*args, **kwargs)
if 'Access-Control-Allow-Origin' not in result.headers:
result.headers.add("Access-Control-Allow-Origin", ', '.join(origin))
return result
wrapper.__name__ = func.__name__
if 'methods' in options:
if 'OPTIONS' in options['methods']:
return app.route(path, **options)(wrapper)
else:
options['methods'].append('OPTIONS')
return app.route(path, **options)(wrapper)
return wrapper
return inner
#corsapp_route('/', methods=['POST'], origin=['*'])
def hello_world():
...
If you can't find your problem and you're code should work, it may be that your request is just reaching the maximum of time heroku allows you to make a request. Heroku cancels requests if it takes more than 30 seconds.
Reference: https://devcenter.heroku.com/articles/request-timeout