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',
},
);
Related
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.
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"])
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)
I have a Python function using the preview option of sending custom metrics to Azure using the REST API https://learn.microsoft.com/en-us/azure/azure-monitor/platform/metrics-store-custom-rest-api, previously this was a C# function where authorisation and getting a bearer token was handled automagically by:
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string bearerToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://monitoring.azure.com/").ConfigureAwait(false);
This worked in VS Code using the logged in user and in Azure when a Managed Identity was assigned to the Function.
I needed to convert this to Python but so far the best (working) I've been able to come up with is:
import logging, requests, os, adal
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
regional_monitoring_url = "https://eastus.monitoring.azure.com"
monitored_resource_id = os.environ['RESOURCE_ID']
full_endpoint = f"{regional_monitoring_url}{monitored_resource_id}/metrics"
tenant_id = os.environ['AZURE_TENANT_ID']
context = adal.AuthenticationContext(f'https://login.microsoftonline.com/{tenant_id}')
token = context.acquire_token_with_client_credentials("https://monitoring.azure.com/", os.environ['AZURE_CLIENT_ID'], os.environ['AZURE_CLIENT_SECRET'] )
bearer_token = token['accessToken']
json = req.get_json()
headers = {"Authorization": 'Bearer ' + bearer_token}
result = requests.post(url = full_endpoint, headers = headers, json = json)
return func.HttpResponse(f"Done - {result.status_code} {result.text}", status_code=200)
This obviously relies on me creating a Service Principal with the relevant permissions. I'm trying to work out how to use the automatic Managed Identity authorisation that the C# libraries have.
I know ADAL should be replaced by MSAL but I can't work out how/if that automagically handles Managed Identities so I tried azure-identity:
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
token = credential.get_token("https://monitoring.azure.com/.default")
bearer_token = token.token
This gets me a token but because it requires a scope rather than a resource, which means adding .default to the resource URL, when I send the bearer token to the monitoring endpoint it complains the resource doesn't match and must be exactly "https://monitoring.azure.com/"
Is this just not currently possible or am I missing something with either azure-identity or the MSAL Python modules?
According to my research, when werequest an Azure AD token to emit custom metrics, ensure that the audience the token is requested for is https://monitoring.azure.com/. For more details, please refer to here. So we should update scope as https://monitoring.azure.com//.default
For example
def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Python HTTP trigger function processed a request.')
credential = DefaultAzureCredential()
token = credential.get_token("https://monitoring.azure.com//.default")
bearer_token = token.token
#full_endpoint=""
json = req.get_json()
headers = {"Authorization": 'Bearer ' + bearer_token}
#result = requests.post(url = full_endpoint, headers = headers, json = json)
return func.HttpResponse(f"Done - {bearer_token}", status_code=200)
every time I succesfully login and want to redirect to /welcome i get an error and don't know why.
Here is my code:
import secrets
from typing import Dict
from fastapi import FastAPI, HTTPException, status, Depends, Cookie, Form, Response, Request
from pydantic import BaseModel
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from hashlib import sha256
app = FastAPI()
security = HTTPBasic()
app.secret_key = "very constatn and random secret, best 64 characters"
app.sessions= []
#app.get("/welcome")
def welcome(request: Request, token = Cookie(None)):
if token in app.sessions:
return {"message": "No hej"}
else:
raise HTTPException(status_code=401, detail="Login first")
#app.post("/login")
def Login(response: Response,credentials: HTTPBasicCredentials = Depends(security)):
correct_username = secrets.compare_digest(credentials.username, "root")
correct_password = secrets.compare_digest(credentials.password, "toor")
if (correct_username and correct_password):
session_token = sha256(bytes(f"{credentials.username}{credentials.password}{app.secret_key}", encoding='utf8')).hexdigest()
response.status_code = status.HTTP_302_FOUND
response.set_cookie(key="session_token", value=session_token)
app.sessions += session_token
response.headers['Location'] = "/welcome"
return response
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect login or password",
)
And here is the error message:
response.set_cookie(key="session_token", value=session_token)
AttributeError: 'Response' object has no attribute 'set_cookie'
In /docs after logging in I get:
500 Internal Server Error
If I switch /login from POST to GET and after logging in it shows:
{"detail":[{"loc":["body","response"],"msg":"field required","type":"value_error.missing"}]}
Any suggestions?
I think using RedirectResponse in fastapi.responses works.
If I understand correctly what you're trying to accomplish, it's this:
https://en.wikipedia.org/wiki/Post/Redirect/Get
And RedirectResponse allows this:
https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse
However, the default status code for RedirectResponse is 307. This means if you make a GET request, the redirect will be a GET request too. And, if you make a POST request, the redirect will be a POST request as well. (I tested this.) If you change the code to 303, though, the POST request will get turned into a GET request upon redirect.
I simplified your code, and the following seems to work:
app = FastAPI()
session_token = "abc"
#app.get("/welcome")
def welcome():
return {"message": "No hej"}
#app.post("/login")
def Login():
rr = RedirectResponse('/welcome', status_code=303)
rr.set_cookie(key="session_token", value=session_token)
return rr
I had the same problem on Ubuntu server with gunicorn and uvicorn. Everything was fine in HTTP. When I added SSL certificate I then would get error in redirects. Solved using #Michael Kennedy solution in FastAPI course code:
response = RedirectResponse('/main', status_code=status.HTTP_302_FOUND)
return response
If setting a cookie:
#api.get('/auth_cookie')
def auth_cookie(response: Response, val):
'''set an auth cookie
'''
# IMPORTANT: httponly=True is important for server, but does not work localhost
response.set_cookie(my_cookie, val, secure=False,
httponly=False, samesite='Lax')
# then later, in your endpoint:
response = RedirectResponse('/main', status_code=status.HTTP_302_FOUND)
auth_cookie(response, access_token)
return response
This is a redirect to a get endpoint. I do not believe this will work with a post.