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
Related
The JSON response of my endpoint returns {} even though I am logging the correct data.
from flask_smorest import Blueprint
bp = Blueprint("auth", __name__, url_prefix="/api/v1/auth/")
#bp.route("/login", methods=["POST"])
#bp.arguments(LoginRequest)
#bp.response(200, JwtTokenResponse)
#bp.response(404, ErrorResponse)
def login(args):
current_app.logger.debug(args)
username = args.get("username", None)
password = args.get("password", None)
current_app.logger.debug(f"Username: {username}")
current_app.logger.debug(f"Password: {password}")
user = User.query.filter_by(username=username).first()
if user is None:
return dict(message="User does not exists"), 404
if not check_password_hash(user.password, password):
return dict(message="Unable to Authenticate user."), 404
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
response = dict(access_token=access_token, refresh_token=refresh_token)
current_app.logger.debug(f"Response: {response}")
return response, 200
My LoginTokenSchema and ErrorResponse schemas are defined as:
from marshmallow import Schema, fields
class JwtTokenResponse(Schema):
access_token = fields.String()
refresh_token = fields.String()
class ErrorResponse(Schema):
message = fields.String()
When I test the API with a user not in the database or with the wrong password; it will product the correct response with ErrorRespose however with the correct creds it just will output {}, when I check the flask logs I can see access/refresh token dict, what am I doing wrong?
You don't need to call Blueprint.response twice, which is what I did in my question. You can call alt_response with a custom error message or just use abort and let flask-smorest take care of the rest.
#bp.route("/login", methods=["POST"])
#bp.arguments(LoginRequest)
#bp.response(200, SmorestJWTTokenResponse)
def login(args):
current_app.logger.debug(args)
username = args.get("username", None)
password = args.get("password", None)
user = User.query.filter_by(username=username).first()
if user is None:
abort(404, message="User does not exists.")
if not check_password_hash(user.password, password):
abort(404, message="Unable to Authenticate user.")
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
return dict(access_token=access_token, refresh_token=refresh_token), 200
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 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
I have been struggling while trying to get Flask Authentication work .
I'm using this example: https://github.com/miguelgrinberg/REST-auth/blob/master/api.py
I have a user in mysql database, I can generate a token but everytime I make a cURL request with it, I get Access Unauthorized with Exception Bad Signature.
Here is my code:
Users.py:
# coding=utf-8
from sqlalchemy import Column, String, Date, Integer, Numeric, Enum
from common.base import Base
from sqlalchemy import create_engine, ForeignKey
from sqlalchemy.orm import relationship, deferred
from model.DictSerializable import DictSerializable
from passlib.apps import custom_app_context as pwd_context
from itsdangerous import (TimedJSONWebSignatureSerializer
as Serializer, BadSignature, SignatureExpired)
class Users( Base, DictSerializable ):
__tablename__ = 'users'
id = Column(Integer, primary_key = True)
username = Column(String(32), index = True)
password_hash = Column(String(128))
def __init__( self, username, password ):
self.username = username
self.password = password
def hash_password(self, password):
self.password_hash = pwd_context.encrypt(password)
def verify_password(self, password):
return pwd_context.verify(password, self.password_hash)
def generate_auth_token(self, expiration=3600):
s = Serializer('SECRET KEY', expires_in=expiration)
return s.dumps({'id': self.id})
#staticmethod
def verify_auth_token(token):
s = Serializer('SECRET KEY')
try:
data = s.loads(token)
except SignatureExpired:
return None # valid token, but expired
except BadSignature:
print("Bad signature")
return None # invalid token
user = session.query(User).filter(User.id == data['id']).first()
return user
app.py :
auth = HTTPBasicAuth('Bearer')
#auth.error_handler
def unauthorized():
return make_response(jsonify({'error': 'Unauthorized access'}), 403)
#app.route('/v1/api/users', methods = ['POST'])
def new_user():
username = request.json.get('username')
password = request.json.get('password')
if username is None or password is None:
abort(400) # missing arguments
if session.query(Users).filter(Users.username == username).first() is not None:
abort(400) # existing user
user = Users(username, password)
user.hash_password(password)
session.add(user)
session.commit()
return jsonify({ 'username': user.username }), 201, {'Location': url_for('get_user', id = user.id, _external = True)}
#app.route('/v1/api/users/<int:id>')
def get_user(id):
user = session.query(Users.id).all()
if not user:
abort(400)
return jsonify({'username': user.username})
#auth.verify_password
def verify_password(username_or_token, password):
# first try to authenticate by token
user = Users.verify_auth_token(username_or_token)
if not user:
# try to authenticate with username/password
user = session.query(Users).filter_by(username=username_or_token).first()
if not user or not user.verify_password(password):
return False
g.user = user
return True
#app.route('/v1/api/token')
#auth.login_required
def get_auth_token():
token = g.user.generate_auth_token(3600)
return jsonify({'token': token.decode('ascii'), 'duration': 3600})
Here is the using case in the terminal using cURL:
vagrant#vagrant-ubuntu-trusty-64:~/projects$ curl -u souad:souad -i -X GET http://localhost:5000/v1/api/token
HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Bearer realm="Authentication Required"
Server: Werkzeug/0.14.1 Python/2.7.6
Date: Wed, 03 Oct 2018 11:30:04 GMT
Output of the API:
[2018-10-03 10:01:36,253] INFO in _internal: 127.0.0.1 - - [03/Oct/2018 10:01:36] "GET /v1/api/token HTTP/1.1" 401 -
bad signature
[2018-10-03 11:30:04,710] INFO in _internal: 127.0.0.1 - - [03/Oct/2018 11:30:04] "GET /v1/api/token HTTP/1.1" 401 -
I don't know why I get Exception Bad Signature ???
What is the easiest way to authenticate by Flask and protect some endpoints ??
I am setting up a Flask-RESTful service and have user authentication working. The method that I'm using is:
def generate_auth_token(username, expiration=600):
gen_serial = Serializer(secret_key, expires_in=expiration)
return gen_serial.dumps({'username': username})
I pass the token to the user as follows:
class token(Resource):
decorators = [auth.login_required]
def post(self):
username = g.user
return_token = generate_auth_token(username)
return {'token':return_token.decode()}, 200
And the token is then verified as such so that it does not need to be stored server side:
def verify_auth_token(auth_token):
serial = Serializer(secret_key)
try:
data = serial.loads(auth_token)
except SignatureExpired:
return None
except BadSignature:
return None
serial_user = data['username']
return serial_user
This seems to work well, however I am unsure how to logout the user before the expiration is expired without storing the token serverside. My thought was to pass back a garbage token when the user elects to logout, but I don't think this is an elegant or secure solution.
Any tips would be really helpful!
Rather than a garbage token simply encode no data:
def generate_auth_token(username=None, expiration=600):
gen_serial = Serializer(secret_key, expires_in=expiration)
data = {'username': username} if username is not None else {}
return gen_serial.dumps(data)
Then you can have an invalidate endpoint that requires a login and returns a token without a username:
def invalidate(self):
return_token = generate_auth_token()
return {'token':return_token.decode()}, 200
At that point, you can just handle the possibly missing username field:
def verify_auth_token(auth_token):
serial = Serializer(secret_key)
try:
data = serial.loads(auth_token)
except SignatureExpired:
return None
except BadSignature:
return None
serial_user = data.get('username')
return serial_user