How to use HTTP Basic Auth as separate FastAPI service? - python

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=.

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

fastapi - firebase authentication with JWT's?

I'm trying to use fastapi to return some basic ML models to users.
Currently, I secure user details with firebase auth. I want to use the JWT's users have when using the basic application to authenticate their request for the ML model.
With fastapi, there doesn't seem to be a straightforward answer to doing this.
I've followed two main threads as ways to work out how to do this, but am a bit lost as to how I can simply take the JWT from the header of a request and check it against firebase admin or whathave you?
Following this tutorial and using this package, I end up with something like this,
https://github.com/tokusumi/fastapi-cloudauth . This doesn't really do anything - it doesn't authenticate the JWT for me, bit confused as to if this package is actually worthwhile?
from fastapi import FastAPI, HTTPException, Header,Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi_cloudauth.firebase import FirebaseCurrentUser, FirebaseClaims
app = FastAPI()
security = HTTPBearer()
origins = [
xxxx
]
app.add_middleware(
xxxx
)
get_current_user = FirebaseCurrentUser(
project_id=os.environ["PROJECT_ID"]
)
#app.get("/user/")
def secure_user(current_user: FirebaseClaims = Depends(get_current_user)):
# ID token is valid and getting user info from ID token
return f"Hello, {current_user.user_id}"
Alternatively, looking at this,
https://github.com/tiangolo/fastapi/issues/4768
It seems like something like this would work,
security = HTTPBearer()
api = FastAPI()
security = HTTPBearer()
firebase_client = FirebaseClient(
firebase_admin_credentials_url=firebase_test_admin_credentials_url
# ...
)
user_roles = [test_role]
async def firebase_authentication(token: HTTPAuthorizationCredentials = Depends(security)) -> dict:
user = firebase_client.verify_token(token.credentials)
return user
async def firebase_authorization(user: dict = Depends(firebase_authentication)):
roles = firebase_client.get_user_roles(user)
for role in roles:
if role in user_roles:
return user
raise HTTPException(detail="User does not have the required roles", status_code=HTTPStatus.FORBIDDEN)
#api.get("/")
async def root(uid: str = Depends(firebase_authorization)):
return {"message": "Successfully authenticated & authorized!"}
But honestly I'm a bit confused about how I would set up the firebase environment variables, what packages I would need (firebaseadmin?)
Would love some helpers, thanks!
I hope this will help:
Create functions to work with Firebase admin, create credentials from Firebase as JSON file:
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Depends, HTTPException, status, Response
from firebase_admin import auth, credentials, initialize_app
credential = credentials.Certificate('./key.json')
initialize_app(credential)
def get_user_token(res: Response, credential: HTTPAuthorizationCredentials=Depends(HTTPBearer(auto_error=False))):
if cred is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Bearer authentication is needed",
headers={'WWW-Authenticate': 'Bearer realm="auth_required"'},
)
try:
decoded_token = auth.verify_id_token(credential.credentials)
except Exception as err:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid authentication from Firebase. {err}",
headers={'WWW-Authenticate': 'Bearer error="invalid_token"'},
)
res.headers['WWW-Authenticate'] = 'Bearer realm="auth_required"'
return decoded_token
Then put it inside your FastAPI main function:
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Depends, HTTPException, status, Response, FastAPI, Depends
from firebase_admin import auth, credentials, initialize_app
credential = credentials.Certificate('./key.json')
initialize_app(credential)
def get_user_token(res: Response, credential: HTTPAuthorizationCredentials=Depends(HTTPBearer(auto_error=False))):
if cred is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Bearer authentication is needed",
headers={'WWW-Authenticate': 'Bearer realm="auth_required"'},
)
try:
decoded_token = auth.verify_id_token(credential.credentials)
except Exception as err:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid authentication from Firebase. {err}",
headers={'WWW-Authenticate': 'Bearer error="invalid_token"'},
)
res.headers['WWW-Authenticate'] = 'Bearer realm="auth_required"'
return decoded_token
app = FastAPI()
#app.get("/api/")
async def hello():
return {"msg":"Hello, this is API server"}
#app.get("/api/user_token")
async def hello_user(user = Depends(get_user_token)):
return {"msg":"Hello, user","uid":user['uid']}
P.S. Do not forget to install requirements:
pip3 install firebase_admin

In authlib, does authorize_access_token require the explicit passing of client_id and client_secret?

I am using authlib with a pretty standard OAuth2 flow.
import pickle
import json
import os.path
from requests.models import Response
from authlib.integrations.flask_client import OAuth, OAuthError
class BaseApi(object):
def __init__(self, oauth_client, config_directory, token_file=''):
self.oauth_client = oauth_client
if token_file:
self.token_file = token_file
else:
self.token_file = os.path.join(config_directory, 'token.pickle')
#property
def token(self):
try:
print ("Token: %s" % self.__token)
return self.__token
except AttributeError:
if os.path.exists(self.token_file):
with open(self.token_file, 'rb') as f:
self.__token = pickle.load(f)
print ("Pickled Token: %s" % self.token)
return self.__token
#token.setter
def token(self, new_token):
self.__token = new_token
# The authorizaiton flow sends us to the OAuth provider
# with a redirect back to our app
def login(self, state=None):
return self.oauth_client.authorize_redirect(self.auth_callback, state=state)
# Our authorized endpoint.
def authorized(self,request):
# Get the access token!
token = self.oauth_client.authorize_access_token(client_id=self.oauth_client.client_id,
client_secret=self.oauth_client.client_secret)
#if resp is None or resp.get('access_token') is None:
# return 'Access denied: error=%s resp=%s' % (
# request.args,
# resp
# )
self.token = token
This is all subclassed in a different module:
from __future__ import print_function
import json
from backend.oauth_base import BaseApi, OAuth, load_credentials_file
from urllib.parse import urlparse
from backend import app, url_for # This is the flask app
from flask import request, redirect, jsonify
import requests
import os
config_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config')
class BasecampApi(BaseApi):
def __init__(self):
oauth = OAuth(app)
credentials = load_credentials_file(os.path.join(config_directory,
'credentials.json'))
oauth.register(
name='basecamp',
client_id=credentials['client_id'],
client_secret=credentials['client_secret'],
api_base_url='https://3.basecampapi.com/',
request_token_url=None,
request_token_params={'type': 'web_server'},
access_token_url='https://launchpad.37signals.com/authorization/token',
access_token_params={'type': 'web_server'},
authorize_url='https://launchpad.37signals.com/authorization/new',
authorize_params={'type': 'web_server'}
)
oauth_client = oauth.create_client('basecamp')
super().__init__(oauth_client, config_directory)
Everything here works, but I'm confused as to why I need to pass client_id and client_secret explicitly in the authorize access token.
token = self.oauth_client.authorize_access_token()
Causes the basecamp API to throw an error complaining about a lack of clien_id (and then client_secret).
This is different from the docs (which I admittedly found confusing). Is this the expected behavior?
The default token_endpoint_auth_method is client_secret_basic, which will send client credentials in HTTP Authorization header. However, according to your description, it seems that the provider needs client_id and client_secret in payload, which should be client_secret_post. Consider to add below client_kwargs into your .register:
client_kwargs = {
'token_endpoint_auth_method': 'client_secret_post',
}

OAuth Facebook Login doesn't work

I'm trying to develop a web app using Google App Engine.
The language i'm using is Python and i'm using the facebook-sdk (https://facebook-sdk.readthedocs.io/en/latest/).
I'm stuck at making the OAuth Facebook login works correctly.
Every single time I attempt to log in to my app using Facebook login i get the error
URL Blocked: This redirect failed because the redirect URI is not whitelisted in the app’s Client OAuth Settings. Make sure Client and Web OAuth Login are on and add all your app domains as Valid OAuth Redirect URIs.
You can see the screenshot of the error (in the original language) below
That are the settings of my Facebook app
That's the code of my app's RequestHandler which handle the login's requests
import facebook
import os
import webapp2
import urllib
from webapp2_extras import sessions
facebook_ID = 'xxxxxxxxxxx'
facebook_secret = 'xxxxxxxxxxxxxxxxxxxxxx'
facebook_graphUrl = 'https://graph.facebook.com/v2.8'
facebook_redirectUri = 'http://gold-circlet-160109.appspot.com/login/'
facebook_requestCode = 'https://www.facebook.com/v2.8/dialog/oauth?' +
urllib.urlencode(
{'client_id':facebook_ID,
'redirect_uri':facebook_redirectUri,
'scope':'public_profile, email, user_friends'})
def retrieve_access_token(code):
args = {'redirect_uri': facebook_redirectUri,
'client_id': facebook_ID,
'client_secret': facebook_secret,
'code': code}
access_token = urllib.urlopen(facebook_graphUrl + "/oauth/access_token?" + urllib.urlencode(args)).read()
access_token = urlparse.parse_qs(access_token)
return access_token['access_token'][0]
def get_graph_api(token):
if isinstance(token, dict):
return facebook.GraphAPI(token['access_token'])
else:
return facebook.GraphAPI(token)
class BaseHandler(webapp2.RequestHandler):
def dispatch(self):
self.session_store = sessions.get_store(request=self.request)
try:
webapp2.RequestHandler.dispatch(self)
finally:
self.session_store.save_sessions(self.response)
#webapp2.cached_property
def session(self):
return self.session_store.get_session()
class FBLogin(BaseHandler):
def get(self):
code = self.request.get('code')
if self.request.get("code"):
access_tk = retrieve_access_token(code)
facebookObject = get_graph_api(access_tk)
facebookObject = facebookObject.get_object('me')
self.session["ECommerceUser"] = dict(
idSocial = facebookObject['id'],
image='http://graph.facebook.com/'+facebookObject['id']+'/picture?type=square',
username=last_name[0] + " " + last_name[1],
loginMethod=loginMethod
)
self.redirect("/")
else:
self.redirect(facebook_requestCode)
I set the same redirect URI in the URL login request is set in the Facebook Login settings, as you can see from the code above (i followed this guide: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow)
Any suggestion?

spotipy authorization code flow

I am using the Spotipy python library to interact with the Spotify web api. I have worked through the API and docs but I do not see a clear example that shows how the library supports the Authorization code flow ( https://developer.spotify.com/web-api/authorization-guide/#authorization-code-flow ).
I implemented a simple Authorization Code flow with the help of Spotipy. Maybe this is helpful for other people as well. Also on github: https://github.com/perelin/spotipy_oauth_demo
Here is the code:
from bottle import route, run, request
import spotipy
from spotipy import oauth2
PORT_NUMBER = 8080
SPOTIPY_CLIENT_ID = 'your_client_id'
SPOTIPY_CLIENT_SECRET = 'your_client_secret'
SPOTIPY_REDIRECT_URI = 'http://localhost:8080'
SCOPE = 'user-library-read'
CACHE = '.spotipyoauthcache'
sp_oauth = oauth2.SpotifyOAuth( SPOTIPY_CLIENT_ID, SPOTIPY_CLIENT_SECRET,SPOTIPY_REDIRECT_URI,scope=SCOPE,cache_path=CACHE )
#route('/')
def index():
access_token = ""
token_info = sp_oauth.get_cached_token()
if token_info:
print "Found cached token!"
access_token = token_info['access_token']
else:
url = request.url
code = sp_oauth.parse_response_code(url)
if code:
print "Found Spotify auth code in Request URL! Trying to get valid access token..."
token_info = sp_oauth.get_access_token(code)
access_token = token_info['access_token']
if access_token:
print "Access token available! Trying to get user information..."
sp = spotipy.Spotify(access_token)
results = sp.current_user()
return results
else:
return htmlForLoginButton()
def htmlForLoginButton():
auth_url = getSPOauthURI()
htmlLoginButton = "<a href='" + auth_url + "'>Login to Spotify</a>"
return htmlLoginButton
def getSPOauthURI():
auth_url = sp_oauth.get_authorize_url()
return auth_url
run(host='', port=8080)
If someone needs the working code here is my current.
Just remember to change the client_id, etc. I put them in config.py.
import spotipy
import spotipy.util as util
from config import CLIENT_ID, CLIENT_SECRET, PLAY_LIST, USER
import random
token = util.oauth2.SpotifyClientCredentials(client_id=CLIENT_ID, client_secret=CLIENT_SECRET)
cache_token = token.get_access_token()
spotify = spotipy.Spotify(cache_token)
results1 = spotify.user_playlist_tracks(USER, PLAY_LIST, limit=100, offset=0)
When I was trying to do this none of these answers really got me there unfortunately. When I ended up figuring it out I detailed how in this post: https://stackoverflow.com/a/42443878/2963703
I was using Django as my backend but all the spotify api oauth stuff is done in javascript so it should still be very useful for you.
The Spotipy library supports the Authorization Code flow, as documented here. For more information, you could also check out Spotipy's oAuth2 module and Util module.

Categories