FastAPI auth check before granting access to sub-applications - jwt

I am mounting a Flask app as a sub-application in my root FastAPI app, as explained in the documentation
Now I want to add an authentication layer using HTTPAuthorizationCredentials dependency, as nicely explained in this tutorial
tutorial code
How can I do that?
Preferably, I would like that any type of access attempt to my Flask sub-application goes first through a valid token authentication process implemented in my FastAPI root app. Is that possible?

You can use a custom WSGIMiddleware and authorize the call to flask app inside that like this:
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.wsgi import WSGIMiddleware
from flask import Flask, escape, request
from starlette.routing import Mount
from starlette.types import Scope, Receive, Send
flask_app = Flask(__name__)
def authenticate(authorization: str = Header()):
# Add logic to authorize user
if authorization == "VALID_TOKEN":
return
else:
raise HTTPException(status_code=401, detail="Not Authorized")
class AuthWSGIMiddleware(WSGIMiddleware):
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
_, authorization = next((header for header in scope['headers'] if header[0] == b'authorization'), (b'authorization', "" ))
authenticate(authorization.decode('utf-8'))
await super().__call__(scope, receive, send)
routes = [
Mount("/v1", AuthWSGIMiddleware(flask_app)),
]
# OR Optionally use this as you were doing
# The above one is preferred as per starlette docs
# app.mount("/v1", WSGIMiddleware(flask_app))
#flask_app.route("/")
def flask_main():
name = request.args.get("name", "World")
return f"Hello, {escape(name)} from Flask!"
app = FastAPI(routes=routes, dependencies=[Depends(authenticate)])
#app.get("/v2")
def read_main():
return {"message": "Hello World"}
For the tutorial you are looking at, you can replace the invoking authenticate function in my example inside AuthWSGIMiddleware->__call__() with
AuthHandler().decode_toke(authorization)

Related

Why do I get a 200 from a authentication-required request using expired access token with django rest framework simple jwt?

I am building a REST API with DRF using djangorestframework-simplejwt(5.2.1)
When I try to access a protected endpoint which requires an authentication with an expired token, expecting a 401. Instead, I get a positive answer from the API every single time.
Here is the test view:
from rest_framework.decorators import permission_classes
from rest_framework.permissions import IsAuthenticated
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def test(request:HttpRequest) -> Response:
return JsonResponse('OK')
Here is the urls.py:
from django.contrib import admin
from django.urls import path,include
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
from account.views import AccountTokenObtainPairView
from base.views import test
urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls')),
path('api/test/', test),
path('api/token/', AccountTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
And here, the postman request with the result I got:
As last my settings.py :
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES':('rest_framework_simplejwt.authentication.JWTAuthentication',),
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
#'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'JWK_URL': None,
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
by calling /api/test/ I was expected django to send back an answer telling me that the request could not be fullfilled since the access token was expired.
I did check that the token is indeed expired:
Thank you for your help!

Flask OIDC with Keycloak returning None when trying to get 'openid_id' and 'role' fields

I'm trying to build some test applications using Flask-ODBC + Keycloak.
I successfully started Keycloak, created a Realm, a role and an user to who this role is assigned. Now I'm trying to get the 'role' and 'openid_id' fields from the OpenIDConnect object but these two fields are returning None.
My application code is the following
import json
import flask
from flask import Flask, render_template, g
import flask_login
from flask_login import login_required
from flask_oidc import OpenIDConnect
from model.user import User
app = Flask(__name__)
login_manager = flask_login.LoginManager()
login_manager.init_app(app)
# Our mock database.
users = {'foo#bar.com': {'pw': 'secret'}}
app.config.update({
'SECRET_KEY': 'u\x91\xcf\xfa\x0c\xb9\x95\xe3t\xba2K\x7f\xfd\xca\xa3\x9f\x90\x88\xb8\xee\xa4\xd6\xe4',
'TESTING': True,
'DEBUG': True,
'OIDC_CLIENT_SECRETS': 'client_secrets.json',
'OIDC_ID_TOKEN_COOKIE_SECURE': False,
'OIDC_REQUIRE_VERIFIED_EMAIL': False,
'OIDC_VALID_ISSUERS': ['http://localhost:8080/auth/realms/MyDemo'],
'OIDC_OPENID_REALM': 'http://localhost:5000/oidc_callback'
})
oidc = OpenIDConnect(app)
#app.route('/')
def hello_world():
if oidc.user_loggedin:
return ('Hello, %s, See private '
'Log out') % \
oidc.user_getfield('email')
else:
return 'Welcome anonymous, Log in'
#app.route('/private')
#oidc.require_login
def hello_me():
info = oidc.user_getinfo(['email', 'openid_id', 'role'])
print(info)
return ('Hello, %s (%s)! Return' %
(info.get('email'), info.get('openid_id')))
#app.route('/api')
#oidc.accept_token(True, ['openid'])
def hello_api():
return json.dumps({'hello': 'Welcome %s' % g.oidc_token_info['sub']})
#app.route('/logout')
def logout():
oidc.logout()
return 'Hi, you have been logged out! Return'
if __name__ == '__main__':
app.run('localhost', port=5000)
Inside hello_me() I try to get the 'openid_id' and 'role' and print it. The problem is I'm getting None in these fields. I can get the email correctly.
Can you help me with finding out what mistakes I'm making?
https://flask-oidc.readthedocs.io/en/latest/
user_getinfo(fields, access_token=None)
Returns: The values of the current user for the fields requested. The keys are the field names, values are the values of the fields as indicated by the OpenID Provider. Note that fields that were not provided by the Provider are absent.
Your provider (Keycloak) doesn't expose openid_id details in the token apparently, so field is absent. Very likely you didn't configure OIDC client in the Keycloak correctly. Make sure you have added correct mappers/scopes to used OIDC client in the Keycloak, which expose requested details into openid_id claim.

clients = self.AVAILABLE_CLIENTS[name] KeyError: 'requests' flask authlib client

good day everybody,
having some issues with flask and authlib. Bellow snip of my flash code
from flask import Flask, render_template
from authlib.integrations.flask_client import OAuth
import os
app = Flask(__name__)
app._static_folder = os.path.abspath("static")
app.config.from_object('config')
oauth = OAuth(app)
webex = oauth.register(name='WEBEX', redirect_uri='http://webapp.dcloud.cisco.com:5000/AuthWebex', client_kwargs={
'scope': 'spark:all'
} )
config.py
import os
WEBEX_CLIENT_ID='C3a256be511cdf07e19f272960c44a214aec14b727b108e4f10bd124d31d2112c'
WEBEX_CLIENT_SECRET='secret'
WEBEX_ACCESS_TOKEN_URL='https://api.ciscospark.com/v1/access_token'
WEBEX_REDIRECT_URI='http://localhost:5000/AuthWebex'
WEBEX_SCOPE='spark:all'
when running above code I get the following error:
File "/Users/tneumann/PycharmProjects/untitled/venv/lib/python3.7/site-packages/authlib/integrations/flask_client/oauth_registry.py", line 61, in register
self.use_oauth_clients()
File "/Users/tneumann/PycharmProjects/untitled/venv/lib/python3.7/site-packages/authlib/integrations/_client/oauth_registry.py", line 49, in use_oauth_clients
clients = self.AVAILABLE_CLIENTS[name]
KeyError: 'requests'
looked at examples and did some research, no luck. Can't find any solution...
thanks in adv.
Tobi
UPDATE:
per comment bellow here the latest code:
from flask import Flask, render_template, url_for, request
from authlib.integrations.flask_client import OAuth
import os
import requests
app = Flask(__name__)
app._static_folder = os.path.abspath("static")
app.config.from_object('config')
app.secret_key = os.urandom(24)
oauth = OAuth(app)
oauth.register(
'webex',
api_base_url='https://api.ciscospark.com/v1',
authorize_url='https://api.ciscospark.com/v1/authorize',
access_token_url='https://api.ciscospark.com/v1/access_token',
redirect_uri='http://webapp.dcloud.cisco.com:5000/AuthWebex',
scope='spark:all')
#app.route('/')
def main():
"""Entry point; the view for the main page"""
return render_template('main.html')
#app.route('/authorize')
def authorize():
return render_template('authorize.html')
#app.route('/login')
def login():
#redirect_uri = url_for('AuthWebex', _external=True)
redirect_uri = 'http://webapp.dcloud.cisco.com:5000/AuthWebex'
print(redirect_uri)
return oauth.webex.authorize_redirect(redirect_uri)
#app.route('/AuthWebex')
def AuthWebex():
#print(request.__dict__)
token = oauth.webex.authorize_access_token( authorization_response=request.url,
redirect_uri='http://webapp.dcloud.cisco.com:5000/AuthWebex',
client_id='C3a256be511cdf07e19f272960c44a214aec14b727b108e4f10bd124d31d2112c',
client_secret='secret',
)
print("Token: ", token)
resp = oauth.webex.get('https://api.ciscospark.com/v1/people/me')
profile = resp.json()
print(profile)
# do something with the token and profile
return '<html><body><h1>Authenticated</h1></body></html>'
if __name__ == '__main__':
app.run()
oauth.webex.authorize_access_token function throws and error when called without the parameters. which is strange as most examples I found exactly do that.
client_id and client_secret are set via the config.py file. This works for the oauth.register function but not for the authorize_access_token.
Additional problem is that even with the parameters, it produces a valid token. When I call the get function I get the following error:
File "/Users/tneumann/PycharmProjects/untitled/venv/lib/python3.7/site-packages/requests/models.py", line 317, in prepare
self.prepare_auth(auth, url)
File "/Users/tneumann/PycharmProjects/untitled/venv/lib/python3.7/site-packages/requests/models.py", line 548, in prepare_auth
r = auth(self)
File "/Users/tneumann/PycharmProjects/untitled/venv/lib/python3.7/site-packages/authlib/integrations/requests_client/oauth2_session.py", line 41, in __call__
raise UnsupportedTokenTypeError(description=description)
authlib.integrations._client.errors.UnsupportedTokenTypeError: unsupported_token_type: Unsupported token_type: 'token_type'
here is the format of the token returned from authorize_access_token function
Token: {'access_token': 'YWIzNGU3<secret>tNDQ5_PF84_7cc07dbd-<secret>-5877334424fd', 'expires_in': 1209599, 'refresh_token': 'MjU2ZDM4N2Et<secret>ZmItMTg5_PF84_7cc07dbd-<secret>877334424fd', 'refresh_token_expires_in': 7722014, 'expires_at': 1574863645}
went through the docs, the code on github and debugging in pycharm with no luck, help would be much appreciated!
The problem here is that this AuthWebex is not a standard OAuth service. The response has no token_type. We can fix it with Authlib compliance fix:
Check the example here:
https://docs.authlib.org/en/latest/client/frameworks.html#compliance-fix-for-oauth-2-0
The slack example has the same issue.

How to set HTTP password in gerrit while making a REST call using python-requests?

Using requests module want to query Gerrit server and for this need set authentication in below code. And this needs to be done without using any other module like pygerrit2. And if this can be done using HTTP password generated from gerrit. If this is not doable, share an example if auth has to be passed along with Get request.
import requests
import json
import os
GERRIT_SERVER = 'https://gerrit.abc.com'
class GerritServer():
def __init__(self):
self._base_url = GERRIT_SERVER
self._endpoints = {
'CHANGES': 'changes',
}
def _get_endpoint(self, token):
try:
endpoint = self._endpoints[token]
except ValueError:
raise Exception('endpoint not defined for {0}'.format(token))
return '{0}/{1}'.format(self._base_url, endpoint)
def get_endpoint_changes(self):
return self._get_endpoint('CHANGES')
Gerrit's REST API uses digest auth.
username = 'foo'
httpassword = 'bar'
api = 'baz'
auth = requests.auth.HTTPDigestAuth(username, httpassword)
r = requests.get(api, auth=auth)

How to use gspread with python-social-auth

I'm trying to use gspread with python-social-auth, I followed the sample describe on documentation and I've created this class to use as credential store:
class Credentials(object):
def __init__ (self, access_token=None, scope=None):
self.access_token = access_token
self.scope=scope
def refresh (self, http):
# get new access_token
# this only gets called if access_token is None
pass
And at my code I've used:
import gspread
credentials = Credentials(access_token=user.social_auth.get_access_token(),
scope=['https://spreadsheets.google.com/feeds'])
sh = gspread.authorize(credentials)
And asking any response from API using:
sh.get_spreadsheets_feed()
This error appears on the console:
*** RequestError: (401, '401: <HTML>\n<HEAD>\n<TITLE>Token invalid - AuthSub token has wrong scope</TITLE>\n</HEAD>\n<BODY BGCOLOR="#FFFFFF" TEXT="#000000">\n<H1>Token invalid - AuthSub token has wrong scope</H1>\n<H2>Error 401</H2>\n</BODY>\n</HTML>\n')
I have defined the scope at my settings and this is working well, for example trying to get contacts from Google Contacts
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/contacts.readonly',
'https://www.googleapis.com/auth/spreadsheets.readonly'
]
Any ideas?
You have to add these scopes also to Python Social Auth settings:
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
...
'https://www.googleapis.com/auth/spreadsheets.readonly',
'https://spreadsheets.google.com/feeds',
'https://www.googleapis.com/auth/drive.file'
]