I am trying to integrate oauth2 with fastapi running with mock oidc-server authentication. I went through the documentation but not able to make out what fits where. This is a snippet from two files -
main.py
from authlib.integrations.starlette_client import OAuth
oauth = OAuth()
CONF_URL = "https://localhost:8080/.well-known/openid-configuration"
oauth.register(
name="cad",
server_metadata_url=CONF_URL,
client_id=settings.CLIENT_ID,
client_secret=settings.CLIENT_SECRET,
client_kwargs={"scope": "openid email profile authorization_group"},
)
#app.middleware("http")
async def authorize(request: Request, call_next):
if not (request.scope["path"].startswith("/login") or request.scope["path"].startswith("/auth")):
if not is_session_okay(request.session):
return RedirectResponse(url="/login")
return await call_next(request)
#app.get("/login")
async def login(request: Request):
redirect_uri = request.url_for("auth")
return await oauth.cad.authorize_redirect(request, redirect_uri)
#app.get("/auth")
async def auth(request: Request):
try:
token = await oauth.cad.authorize_access_token(request)
except OAuthError as error:
return HTMLResponse(f"<h1>{error.error}</h1>")
user = await oauth.cad.parse_id_token(request, token)
request.session["user"] = dict(user)
request.session["session_expiry"] = str(datetime.datetime.utcnow() +
datetime.timedelta(hours=48))
return {"access_token": create_token(user['sub'), "token_type": "bearer"}
& jwt.py
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/auth', auto_error=False)
# Error
CREDENTIALS_EXCEPTION = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate credentials',
headers={'WWW-Authenticate': 'Bearer'},
)
# Create token internal function
def create_access_token(*, data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({'exp': expire})
encoded_jwt = jwt.encode(to_encode, API_SECRET_KEY, algorithm=API_ALGORITHM)
return encoded_jwt
def create_refresh_token(email):
expires = timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)
return create_access_token(data={'sub': email}, expires_delta=expires)
def create_token(id):
access_token_expires = timedelta(minutes=API_ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={'sub': id}, expires_delta=access_token_expires)
return access_token
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = decode_token(token)
id: str = payload.get('sub')
if email is None:
raise CREDENTIALS_EXCEPTION
except jwt.JWTError:
raise CREDENTIALS_EXCEPTION
raise id
I have tried integrating create_token in the "auth" endpoint and adding Depends(get_current_user) parameter in get api . I get the authorize button in swagger, but authorization doesn't happen with client id & client secret, nor with user-name & passwd.
I would like to connect to spotify API and connect with OAuth and then search for a song a user requests.
I currently have a bot pulling for youtube and saving the file to play. I was wondering if there a way to do the same with Spotipy.
from flask import Flask, request, url_for, session, redirect
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import time
app = Flask(__name__)
app.secret_key = 'xxx'
app.config['SESSION_COOKIE_NAME'] = 'Token Cookie'
TOKEN_INFO = "token_info"
#app.route('/')
def login():
sp_oauth = create_spotify_oauth()
auth_url = sp_oauth.get_authorize_url()
return redirect(auth_url)
#app.route('/redirect')
def redirectPage():
sp_oauth = create_spotify_oauth()
session.clear()
code = request.args.get('code')
token_info = sp_oauth.get_access_token(code)
session[TOKEN_INFO] = token_info
return redirect(url_for('getTracks', _external=True))
#app.route('/getTracks')
def getTracks():
try:
token_info = get_token()
except:
print("user not logged in)")
redirect("/")
sp = spotipy.Spotify(auth = token_info["access_token"])
return
def get_token():
token_info = session.get(TOKEN_INFO, None)
if not token_info:
raise "execption"
now = int(time.time())
is_expired = token_info["expires_at"] - now < 60
if (is_expired):
sp_oauth = create_spotify_oauth()
token_info = sp_oauth.refresh_access_token(token_info['refresh_token'])
return token_info
def create_spotify_oauth():
return SpotifyOAuth(
client_id = 'CLIENT_ID',
client_secret = 'CLIENT_SECRET',
redirect_uri = url_for('redirectPage', _external = True),
scope= 'user-library-read')
I have the following app.py:
from flask import Flask
from waitress import serve
from bprint import api_blueprint
from errors import invalid_id, not_found, invalid_input, internal_server_error, unauthorized_access
app = Flask(__name__)
app.register_blueprint(api_blueprint)
app.register_error_handler(400, invalid_id)
app.register_error_handler(401, unauthorized_access)
app.register_error_handler(404, not_found)
app.register_error_handler(405, invalid_input)
app.register_error_handler(500, internal_server_error)
if __name__ == "__main__":
serve(app, host='localhost')
And the following code in bprint.py:
from flask import Blueprint, jsonify, request
import dbu
from models import Session, user_table, car_table, order_table
from schema import UserDetails, UserQuery, OrderDetails, OrderQuery, CarDetails, CarQuery, LoginData, \
ListUsersReq, Response
from contextlib import contextmanager
from flask_jwt_extended import jwt_required, create_access_token, get_jwt_identity
import datetime
api_blueprint = Blueprint('api', __name__)
#contextmanager
def session_scope():
session = Session()
try:
yield session
session.commit()
except:
session.rollback()
raise
else:
try:
session.commit()
except:
session.rollback()
raise
#api_blueprint.route("/login", methods=["POST"])
def login():
from app import bcrypt
data = LoginData().load(request.json)
if data:
user = dbu.get_entry_by_username(user_table, username=data["username"])
hpw = bcrypt.generate_password_hash(data["password"])
if not user:
return jsonify({"message": "Couldn't find user!"})
if bcrypt.check_password_hash(hpw, data["password"]):
access_token = create_access_token(identity=data["username"], expires_delta=datetime.timedelta(days=365))
return jsonify(access_token=access_token, id=user.id), 200
#api_blueprint.route("/user", methods=["GET"])
def list_users():
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin:
args = ListUsersReq().load(request.args)
userlist = dbu.list_users(args.get("email"), args.get("username"))
return jsonify(UserDetails(many=True).dump(userlist))
else:
return jsonify(code=401, type='UNAUTHORIZED_ACCESS'), 401
#api_blueprint.route("/user", methods=["POST"])
def create_user():
with session_scope():
from app import bcrypt
user_details = UserQuery().load(request.get_json(force=True))
user_details["password"] = bcrypt.generate_password_hash(user_details["password"]).decode('UTF-8')
user = dbu.create_entry(user_table, **user_details)
access_token = create_access_token(identity=user.username, expires_delta=datetime.timedelta(days=365))
return jsonify(access_token=access_token, id=UserDetails().dump(user)["id"]), 200
#api_blueprint.route("/user/<int:id>", methods=["GET"])
def user_by_id(id):
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin:
user = dbu.get_entry_by_id(user_table, id)
return jsonify(UserDetails().dump(user))
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/user/<int:id>", methods=["PUT"])
def update_user(id):
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin or user.id == id:
user_details = UserQuery().load(request.json)
user = dbu.get_entry_by_id(user_table, id)
dbu.update_entry(user, **user_details)
return jsonify(Response().dump({"code": "200"}))
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/user/<int:id>", methods=["DELETE"])
def delete_user(id):
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin or user.id == id:
dbu.delete_entry(user_table, id)
return jsonify(Response().dump({"code": "200"}))
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/cars", methods=["GET"])
def get_inventory():
with session_scope():
cars = dbu.list_cars()
return jsonify(CarDetails(many=True).dump(cars))
#api_blueprint.route("/cars/car/<int:carId>", methods=["GET"])
def get_car_by_id(carId):
with session_scope():
car = dbu.get_car_by_id(car_table, carId)
return jsonify(CarDetails().dump(car))
#api_blueprint.route("/cars/car", methods=["POST"])
def create_car():
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin:
car_details = CarQuery().load(request.json)
car = dbu.create_entry(car_table, **car_details)
return jsonify({"carId": CarDetails().dump(car)["carId"]})
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/cars/car/<int:carId>", methods=["PUT"])
def update_car(carId):
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin:
car_details = CarQuery().load(request.json)
car = dbu.get_car_by_id(car_table, carId)
dbu.update_entry(car, **car_details)
return jsonify(Response().dump({"code": "200"}))
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/cars/car/<int:carId>", methods=["DELETE"])
def delete_car(carId):
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin:
dbu.delete_car(car_table, carId)
return jsonify(Response().dump({"code": "200"}))
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/cars/car/<int:carId>/order", methods=["POST"])
def place_order(carId):
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user:
order_data = OrderQuery().load(request.json)
order = dbu.create_entry(order_table,
userId=user.id,
carId=carId,
shipDate=order_data["shipDate"],
returnDate=order_data["returnDate"],
status="placed",
complete=False)
return jsonify({"id": OrderDetails().dump(order)["id"]})
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/orders", methods=["GET"])
def get_orders():
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user.admin:
orders = dbu.list_orders()
return jsonify(OrderDetails(many=True).dump(orders))
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/cars/car/<int:carId>/order/<int:orderId>", methods=["GET"])
def get_order_by_id(carId, orderId):
with session_scope():
current_user = get_jwt_identity()
user = dbu.get_entry_by_username(user_table, current_user)
if user:
order = dbu.get_entry_by_id(order_table, id=orderId)
return jsonify(OrderDetails().dump(order))
else:
return jsonify(code=401, type="UNAUTHORIZED_ACCESS"), 401
#api_blueprint.route("/cars/car/<int:carId>/order/<int:orderId>", methods=["DELETE"])
def delete_order(carId, orderId):
with session_scope():
dbu.delete_entry(order_table, id=orderId)
return jsonify(Response().dump({"code": "200"}))
When I try to run waitress-serve --port=5000 app:app I get the following error:
app.register_blueprint(api_blueprint)
raise AssertionError(
AssertionError: View function mapping is overwriting an existing endpoint function: api.wrapper
What may be the problem?
I'm almost sure it worked in December and now after reinstalling my Windows it doesn't
Now it also says I have too much code in my question and I don't know how to explain my problem with more words, so I have to add a few useless lines, sorry
Check if you have some custom decorators on views. Because flask take endpoint name either from #route parameter or from function name. In your case there're no endpoint parameter in any of functions.
Error says api.wrapper and it means that you have 2 or more function with name wrapper. Usually we see such name inside decorators. So you probably have decorator that looks like
def decorator(f):
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
And some views are decorated with such decorator. And flask take "wrapper" as a view name
Try by commenting few lines of code - It may help you to resolve your issue.
from flask import Flask
from waitress import serve
from bprint import api_blueprint
# from errors import invalid_id, not_found, invalid_input, internal_server_error, unauthorized_access
app = Flask(__name__)
app.register_blueprint(api_blueprint)
# app.register_error_handler(400, invalid_id)
# app.register_error_handler(401, unauthorized_access)
# app.register_error_handler(404, not_found)
# app.register_error_handler(405, invalid_input)
# app.register_error_handler(500, internal_server_error)
if __name__ == "__main__":
serve(app, host='localhost')
When I was using Flask, every API call is authenticated before processed:
app = connexion.App(__name__, specification_dir='./swagger/', swagger_json=True, swagger_ui=True, server='tornado')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('swagger.yaml', arguments={'title': 'ABCD API'})
# add CORS support
CORS(app.app)
#app.app.before_request
def before_request_func():
app_id = request.headers.get("X-AppId")
token = request.headers.get("X-Token")
user, success = security.Security().authorize(token)
if not success:
status_code = 401
response = {
'code': status_code,
'message': 'Unauthorized user'
}
return jsonify(response), status_code
g.user = user
When I changed it to AioHttp, my authentication is not properly setup:
options = {'swagger_path': 'swagger/', "swagger_ui": True}
app = connexion.AioHttpApp(__name__, specification_dir='swagger/', options=options)
app.add_api('swagger.yaml', arguments={'title': ' ABCD API'})
app = web.Application(middlewares=[auth_through_token])
async def auth_through_token(app: web.Application, handler: Any) -> Callable:
#web.middleware
async def middleware_handler(request: web.Request) -> web.Response:
headers = request.headers
x_auth_token = headers.get("X-Token")
app_id = headers.get("X-AppId")
user, success = security.Security().authorize(x_auth_token)
if not success:
return web.json_response(status=401, data={
"error": {
"message": ("Not authorized. Reason: {}"
)
}
})
response = await handler(request)
return response
return middleware_handler
My request is not getting redirected to the API method.
Could anyone please help me to set up, my before_request authentication for every API?
Thanks.
Firstly, you have to move middleware_handler out from auth_through_token.
Then,
Quote your code:
options = {'swagger_path': 'swagger/', "swagger_ui": True}
app = connexion.AioHttpApp(__name__, specification_dir='swagger/', options=options)
app.add_api('swagger.yaml', arguments={'title': ' ABCD API'})
app = web.Application(middlewares=[auth_through_token])
You have to remove the last line and change the first line to:
options = {'swagger_path': 'swagger/', "swagger_ui": True, 'middlewares': [middleware_handler]}
So finally the code should look like:
options = {'swagger_path': 'swagger/', "swagger_ui": True, 'middlewares': [middleware_handler]}
app = connexion.AioHttpApp(__name__, specification_dir='swagger/', options=options)
app.add_api('swagger.yaml', arguments={'title': ' ABCD API'})
#web.middleware
async def middleware_handler(request: web.Request, handler: Any) -> web.Response:
headers = request.headers
x_auth_token = headers.get("X-Token")
app_id = headers.get("X-AppId")
user, success = security.Security().authorize(x_auth_token)
if not success:
return web.json_response(status=401, data={
"error": {
"message": ("Not authorized. Reason: {}"
)
}
})
response = await handler(request)
return response
Here is my route:
#blueprint.before_request
def load_session_from_cookie():
if request.endpoint != 'client.me':
try:
cookie = request.cookies.get(settings.COOKIE_NAME, None)
# if cookie does not exist, redirect to login url
if not cookie:
session.pop('accountId', None)
return redirect(settings.LOGIN_URL)
account = check_sso_cookie(cookie)
if 'accountId' in session:
return
elif 'accountId' in account:
session['accountId'] = account.get('accountId')
return
else:
session.pop('accountId', None)
return redirect(settings.LOGIN_URL)
except BadSignature:
session.pop('accountId', None)
return redirect(settings.LOGIN_URL)
#blueprint.route('/')
def home():
session.permanent = True
return render_template('index.html')
Here is my test:
from flask import Flask
from flask.ext.testing import TestCase
from api.client import blueprint
class TestInitViews(TestCase):
render_templates = False
def create_app(self):
app = Flask(__name__)
app.config['TESTING'] = True
app.config['SECRET_KEY'] = 'sekrit!'
app.register_blueprint(blueprint)
return app
def setUp(self):
self.app = self.create_app()
self.cookie = '{ "account_id": 100 }'
def test_root_route(self):
resp = self.client.get("/")
self.assert_template_used('index.html')
def test_root_route_404(self):
res = self.client.get('/foo')
self.assertEqual(res.status_code, 404)
The problem is that the test test_root_route fails because a redirect happens because the session doesn't exist. I can't find any good resource online that shows how to incorporate session management with Flask-Tests... anyone have a good way of doing this?
You can make login request before:
def create_app(self):
...
app.config['TESTING'] = True # should off csrf
...
def test_root_route(self):
self.client.post(settings.LOGIN_URL, data={'login': 'l', 'password': 'p'})
resp = self.client.get('/')
self.assert_template_used('index.html')
For some difficult cases login route can be mocked.
Or manually set cookie:
def test_root_route(self):
resp = self.client.get('/', headers={'Cookie': 'accountId=test'})
self.assert_template_used('index.html')
Or set session with session transaction (http://flask.pocoo.org/docs/testing/#accessing-and-modifying-sessions):
def test_root_route(self):
with self.client.session_transaction() as session:
session['accountId'] = 'test'
resp = self.client.get('/')
self.assert_template_used('index.html')