How to integrate oauth2 with fastapi? - python

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.

Related

Import Spotify song request

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')

How to extract jwt token payload when token is expired in python jwt

I am unable to extract JWT token payload after the jwt token expires in python jwt package.
I am using flask api for backend development and the implementation is in the middleware.
Below is my code:
import jwt
from flask import request
from functools import wraps
from werkzeug.exceptions import Forbidden, Unauthorized
def admin_rights_required(f):
#wraps(f)
def _decorated(*args, **kwargs):
config = readConfig()
secretKey = config["JWT_SECRET_KEY"]
algorithm = config["JWT_ENCODING_ALGORITHM"]
token = None
if "Authorization" in request.headers:
data = request.headers["Authorization"]
token = str.replace(str(data), "Bearer ", "")
try:
if not token or (not _ruleUserObj.getRuleUserFromToken(token)):
data = jwt.decode(token, secretKey, algorithms=algorithm)
raise Unauthorized("Token is missing")
data = jwt.decode(token, secretKey, algorithms=algorithm)
if getTokenDurationDifference(token) == -1:
raise jwt.InvalidTokenError
currentUser = _ruleUserObj.getRuleUser(data["sub"]["username"])
if not len(currentUser) > 0:
raise jwt.InvalidTokenError
if currentUser["isAdmin"] == False:
raise Forbidden()
except jwt.ExpiredSignatureError:
_ruleUserObj.updatedRuleUserSessionRemToken(data["sub"]["username"])
raise Unauthorized("Signature expired. Please log in again.")
except jwt.InvalidTokenError:
_ruleUserObj.updatedRuleUserSessionRemToken(data["sub"]["username"])
raise Unauthorized("Invalid token. Please log in again.")
return f(*args, **kwargs)
return _decorated
I have found out the solution in the jwt package in python. Below is the link for reference:
https://pyjwt.readthedocs.io/en/latest/usage.html#reading-the-claimset-without-validation
Below is the code change which I did for the above:
jwt.decode(
token,
secretKey,
algorithms=algorithm,
options={"verify_signature": False},
)
)["sub"]["username"]
)
After merging the code with the main code it looks like below:
import jwt
from flask import request
from functools import wraps
from werkzeug.exceptions import Forbidden, Unauthorized
def admin_rights_required(f):
#wraps(f)
def _decorated(*args, **kwargs):
config = readConfig()
secretKey = config["JWT_SECRET_KEY"]
algorithm = config["JWT_ENCODING_ALGORITHM"]
token = None
if "Authorization" in request.headers:
data = request.headers["Authorization"]
token = str.replace(str(data), "Bearer ", "")
try:
if not token or (not _ruleUserObj.getRuleUserFromToken(token)):
data = jwt.decode(token, secretKey, algorithms=algorithm)
raise Unauthorized("Token is missing")
data = jwt.decode(token, secretKey, algorithms=algorithm)
if getTokenDurationDifference(token) == -1:
raise jwt.InvalidTokenError
currentUser = _ruleUserObj.getRuleUser(data["sub"]["username"])
if not len(currentUser) > 0:
raise jwt.InvalidTokenError
if currentUser["isAdmin"] == False:
raise Forbidden()
except jwt.ExpiredSignatureError:
_ruleUserObj.updatedRuleUserSessionRemToken(
(
jwt.decode(
token,
secretKey,
algorithms=algorithm,
options={"verify_signature": False},
)
)["sub"]["username"]
)
raise Unauthorized("Signature expired. Please log in again.")
except jwt.InvalidTokenError:
_ruleUserObj.updatedRuleUserSessionRemToken(
(
jwt.decode(
token,
secretKey,
algorithms=algorithm,
options={"verify_signature": False},
)
)["sub"]["username"]
)
raise Unauthorized("Invalid token. Please log in again.")
return f(*args, **kwargs)
return _decorated
There is alternative way to decode the jwt token if it is expired:
As suggested by #KlausD. Below is the implementation:
import base64
import json
tokenSplit = token.split(".")
json.loads((base64.b64decode(tokenSplit[1])).decode("utf-8"))
Thanks to #KlausD. for the simple hack
If the options parameter doesn't work like this: options = { "verify_signature": False }
Using the lib pyjwt==1.7.1 try to inform the parameter "verify=False", like this here:
payload = jwt.decode(jwt=token, key=secret, verify=False, algorithms = [ 'HS256' ])

Securing endpoints in fastapi

In my main.py, I have the following code-
app = FastAPI(docs_url="",)
app.add_middleware(SessionMiddleware, secret_key=os.getenv('SECRET'))
config = Config('.env')
oauth = OAuth(config)
CONF_URL = 'http://localhost:9090/.well-known/openid-configuration'
oauth.register(
name='google',
server_metadata_url=CONF_URL,
client_id=os.getenv('ID'),
client_secret=os.getenv('SECRET'),
client_kwargs={
'scope': 'openid email profile'
}
)
api_url = None
#app.get('/')
async def homepage(request: Request):
user = request.session.get('user')
if user:
data = json.dumps(user)
html = (
f'<pre>{data}</pre>'
'logout'
)
return HTMLResponse(html)
return HTMLResponse('login')
#app.get('/login')
async def login(request: Request):
redirect_uri = request.url_for('auth')
return await oauth.google.authorize_redirect(request, redirect_uri)
#app.get('/auth')
async def auth(request: Request):
try:
token = await oauth.google.authorize_access_token(request)
except OAuthError as error:
return HTMLResponse(f'<h1>{error.error}</h1>')
user = await oauth.google.parse_id_token(request, token)
request.session['user'] = dict(user)
if api_url:
return RedirectResponse(url=api_url)
return RedirectResponse(url='/')
#app.get('/logout')
async def logout(request: Request):
request.session.pop('user', None)
request.cookies.clear()
return RedirectResponse(url='/')
#app.get("/api")
async def get_swagger_ui(request: Request):
global api_url
api_url = request.url
user = request.session.get('user')
if user:
return get_swagger_ui_html(
openapi_url="/openapi.json", title="Webapp",)
else:
return RedirectResponse(
url="/login"
)
# routes
PROTECTED = [Depends(login)]
app.include_router(get_api.router, dependencies=PROTECTED)
In the get_api.py file, I have the following conf -
router = APIRouter()
#router.get("/api/higs", tags=["higs"])
def get_higs(db: Session = Depends(get_db),
)
try:
<something>
return x
except Exception as err:
raise HTTPException(
status_code=400, detail="Invalid parameter : {}".format(err),
)
There are similar other endpoints in the get_api.py file. I wanted to block access to these endpoints without authentication. So in the app.include_router method, I added dependencies. But its not working. I am able to access the endpoint data. For e.g. localhost:8000/api/higs - displays all the data in text that I would get from calling executing the GET endpoint in swagger UI. How can I fix this issue. Thanks.

How to redirect with a Oauth token return with FASTAPI?

I am contacting you because I am trying to make a redirection by recovering the token but unfortunately my protected service is not accessible because no token is recovered and I do not know why.
I tried to put the token in the cookie but without result.
Here is my code:
#app.post("/login", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db : Session = Depends(get_db),
username: str = Form(...), password: str = Form(...)):
#user = authenticate_user(db, form_data.username, form_data.password)
user = authenticate_user(db, username, password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
auth = {"access_token": access_token, "token_type": "bearer"}
response = RedirectResponse(url='/licence/',status_code=HTTP_302_FOUND)
response.set_cookie(key='access_token', value=access_token)
return response
#app.get("/licence/")
def form_post(request: Request, auth: User = Depends(get_current_user)):
result = "Type a number"
return templates.TemplateResponse('home.html', context={'request': request, 'result': result})
Here is the code for get_current_user:
async def get_current_user(token: str = Depends(oauth2_scheme), db : Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = crud.get_user_by_username(db, username=token_data.username)
if user is None:
raise credentials_exception
return user
Here is my output: :
enter image description here
enter image description here

Handling the token expiration in fastapi

I'm new with fastapi security and I'm trying to implement the authentication thing and then use scopes.
The problem is that I'm setting an expiration time for the token but after the expiration time the user still authenticated and can access services
import json
from jose import jwt,JWTError
from typing import Optional
from datetime import datetime,timedelta
from fastapi.security import OAuth2PasswordBearer,OAuth2PasswordRequestForm,SecurityScopes
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException,status
from tinydb import TinyDB,where
from tinydb import Query
from passlib.hash import bcrypt
from pydantic import BaseModel
from passlib.context import CryptContext
##
class TokenData(BaseModel):
username: Optional[str] = None
class Token(BaseModel):
access_token: str
token_type: str
router = APIRouter()
SECRET_KEY="e79b2a1eaa2b801bc81c49127ca4607749cc2629f73518194f528fc5c8491713"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=1
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/dev-service/api/v1/openvpn/token")
db=TinyDB('app/Users.json')
Users = db.table('User')
User = Query
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(BaseModel):
username: str
password:str
def get_user(username: str):#still
user= Users.search((where('name') ==name))
if user:
return user[0]
#router.post('/verif')
async def verify_user(name,password):
user = Users.search((where('name') ==name))
print(user)
if not user:
return False
print(user)
passw=user[0]['password']
if not bcrypt.verify(password,passw):
return False
return user
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=1)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
#router.post("/token", response_model=Token)
async def token_generate(form_data:OAuth2PasswordRequestForm=Depends()):
user=await verify_user(form_data.username,form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": form_data.username}, expires_delta=access_token_expires)
return {"access_token": access_token, "token_type": "bearer"}
#router.get('/user/me')
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user =Users.search(where('name') ==token_data.username)
if user is None:
raise credentials_exception
return user
#router.post('/user')
async def create_user(name,password):
Users.insert({'name':name,'password':bcrypt.hash(password)})
return True
How can I really see the expiration of the token and how can I add the scopes?
I 'd wanted to comment on Unyime Etim's advice
but have no rating yet so this would be a separate answer
I just wanted to add that jwt.decode has a built-in method to check "exp"
and it does check it by default (https://github.com/mpdavis/python-jose/blob/96474ecfb6ad3ce16f41b0814ab5126d58725e2a/jose/jwt.py#L82)
so to make sure your token has been expired you can just handle the corresponding exception ExpiredSignatureError
try:
# decode token and extract username and expires data
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
except ExpiredSignatureError: # <---- this one
raise HTTPException(status_code=403, detail="token has been expired")
except JWTError:
raise credentials_exception
I had pretty much the same confusion when I started out with FastAPI. The access token you created will not expire on its own, so you will need to check if it is expired while validating the token at get_current_user. You could modify your TokenData schema to the code below:
class TokenData(BaseModel):
username: Optional[str] = None
expires: Optional[datetime]
And your get_current_user to:
#router.get('/user/me')
async def get_current_user(token: str = Depends(oauth2_scheme)):
# get the current user from auth token
# define credential exception
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# decode token and extract username and expires data
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
expires = payload.get("exp")
except JWTError:
raise credentials_exception
# validate username
if username is None:
raise credentials_exception
token_data = TokenData(username=username, expires=expires)
user = Users.search(where('name') == token_data.username)
if user is None:
raise credentials_exception
# check token expiration
if expires is None:
raise credentials_exception
if datetime.utcnow() > token_data.expires:
raise credentials_exception
return user
Scopes is a huge topic on its own, so I can't cover it here. However, you can read more on it at the fastAPI docs here
you don't need to check if it is expired while validating the token at get_current_user(). I was facing same problem. because...
At first I set expiration time==60,
(ACCESS_TOKEN_EXPIRE_MINUTES = 60)
that generated a token, with that token I was testing api.
than I set expiration time==1 and did not generated a token,
(ACCESS_TOKEN_EXPIRE_MINUTES = 1)
expecting that after one minute the previous token will expire but that did not happen because the previous token had 60 minute life.
so, I created new token , tested api with that token. every thing was fine.
after one minute new token got expired.
The answer above does not account that the token_data.expires needs to be converted to a utc date time object.
# check token expiration
if expires is None:
raise credentials_exception
if datetime.utcnow() > datetime.utcfromtimestamp(token_data.expires):
raise credentials_exception
return user
I would modify the example code which doesnt propogate error as JWT does token signature checks and all you need todo is expose the error by propogating the error.
def get_current_user_from_token(
token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)
):
credentials_exception = lambda x : HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=x,
)
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
username: str = payload.get("sub")
print("username/email extracted is ", username)
if username is None:
raise credentials_exception('Could not validate login or error in username/pass')
except JWTError as e:
raise credentials_exception(str(e))
user = get_user(username=username, db=db)
if user is None:
raise credentials_exception('Could not validate login')
return user

Categories

Resources