I have a dictionary of users with (username, password) as key, value pair.
i would like to restrict the authorisation creation to only users in my dictionary.
So any other user who is not in the dictionary shouldn't be able to create a token.
I try this but it's not working, I can still create token to a new user.
#api.post("/token")
async def get_token(form_data: OAuth2PasswordRequestForm = Depends()):
if not authenticate_user(username=form_data.username,
password=form_data.password):
raise HTTPException(status_code=403, detail='Authentication failed')
else:
return {
"access_token": form_data.username,
"token_type": "bearer"
}
raise HTTPException(status_code=403,
detail='Authentication failed')
You never compare anything against form_data.username - the only thing you do is that you start looping over the user, and you check whether the first users password match - well, the user's password. This will always be true.
Instead, retrieve the user you're looking for and compare the password if present:
#api.post("/token")
async def get_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = users.get(form_data.username)
if user and authenticate_user(form_data=username, password=form_data.password):
return {
"access_token": form_data.username,
"token_type": "bearer"
}
raise HTTPException(status_code=403,
detail='Authentication failed')
I'd also like to point out that you're using the users username as the token. This means that anyone can fake a token by simply supplying the username they want to be authenticated as.
Instead, use secrets.token_urlsafe() to generate a token that can be used as an authentication secret. You'll need to store these tokens somewhere, but in this example it seems like you're storing everything in the currently running Python application for users, so you can do the same for tokens. Define valid_tokens = {} somewhere when initializing your application, then insert rows into this dict to assign a username.
token = secrets.token_urlsafe(32)
valid_tokens[token] = form_data.user_name
return {
"access_token": token,
"token_type": "bearer"
}
You can then verify this token in a separate function and use it to look up the username of the logged in user from the token.
Related
I am making an authenticated client that is a pytest fixture of my fastapi application,the code below shows an authenticated user,that should be able for example to create posts,update and delete them.But when I use it in my tests,it shows an 401 response error (that is returned when user is not logged)
#pytest.fixture()
def authenticated_user(client):
user_data = {
'email': 'julioooo#gmail.com',
'password': '123',
}
res = client.post('/users/create', json={
**user_data,
'name': 'julin',
'job': 'farmer',
})
user_id = res.json() # this returns the user id
# to send a request to api to confirm user creation
user_id = user_id['id']
verify_user = client.post(f'/users/confirm-user/{user_id}').json()
print(verify_user) #print the confirmation message (debugging process)
log_user_res = client.post('/auth/login', json={
**user_data
}) # this returns in the header a httponly cookie...
# response.set_cookie() method
client.headers={**log_user_res.headers}
return client
After some time trying to debug this fixture I thought,well,the headers actually DO HAVE the httponly cookie in it,so the problem is in another part of the code,maybe in the part that checks if the user is logged or not
def get_current_user(
request: Request,
db=Depends(connect)
):
user_id = request.cookies.get('Authorization')
user = User.objects(id=user_id).first()
if not user:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Could not validate credentials or Not Logged or User Does Not Exist",
headers={"WWW-Authenticate": "Bearer"},
)
raise credentials_exception
return user
And I couldn't find the problem,then I realized that this dependency of connect function actually returns a connection from the production db, not from the testing one,soo I created a new one that is :
def test_get_current_user(request: Request, db=Depends(test_connect)):
load_dotenv()
test_connection_url = os.getenv('test_connection_url')
user_id = request.cookies.get('Authorization')
client = MongoClient(test_connection_url)
users = client['TestJC']['users']
user = users.find_one({"_id": user_id})
if not user:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Could not validate credentials or Not Logged or User Does Not Exist",
headers={"WWW-Authenticate": "Bearer"},
)
raise credentials_exception
return user
PS: In the original function I use mongoengine to be more practical,in this one I use pymongo actually because the test_connect function that returns a pymongo connection instance,is used for tests,because it was the only way I found out to fix a bug that I had in my code that every time I deleted the test db after ending up the tests,it also deleted the production db (which is not cool), because Users.objects().delete() did not work and users.drop() did,I am using this way,but returning to the point....
PS2: If you have seen this question before I updated it,sorry for the bad question,I was very sleepy XD
I even overrided the get_current_user dependency to this test one trying to fix the problem
I even created a "headers version" of this authenticated user fixture where I only returned client.headers and in the request just put in the post method headers={**authenticated_headers}
BUT NOTHING SEEMS TO WORK,every single time is just 401..401...AND MORE 401
Thank you guys for your time here I appreciate A LOT :)
Help me understand how user sessions work based on JWT. Im using fastapi with static Jinja Template for now. But I think the question is not about a specific framework, but about the logic of the whole.
Now I have implemented the following functionality:
When a user logs in, a JWT is created which is written to redis in the following form
{
'SOMe_KEY' : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0.yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
}
The token has the necessary information about the user: id, email, permission_type
now, as i understand it, every request which should check for authorization should either check if the user has a session or redirect to the log_in page
My question:
How to match a user with a token in redis, this query here
session = await redis_cache.get('SOMe_KEY')
How to understand which key to pass in the get('SOMe_KEY') query
And which key to use when generating a token record in the database.
I.e. now i have hardcode for one user, there is a session key in the database and i pass the same key in the query. But what should be the logic for a large quantity of users?
Code sample
#router.post('/sign-in')
async def sign_in(
user_data: OAuth2PasswordRequestForm = Depends(),
service: AuthService = Depends(),
):
token = await service.authenticate_user(
user_data.username,
user_data.password
)
await redis_cache.set('SOMe_KEY', token)
#router.get("/private")
def read_private(username: str = Depends(get_current_user)):
return {"username": username, "private": "get some private data"}
async def get_current_user():
try:
session = await redis_cache.get('SOMe_KEY')
payload = jwt.decode(
session,
config('JWT_SECRET'),
algorithms=config('JWT_ALGORITHM'),
)
return payload['user']
except Exception as e:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid authentication"
)
I'm trying to create an auth system for my react(nextjs) app with flask_JWT_extended + ariadne(graphql). I have succesfully set up the login mutation that retrieves the access and refresh token but I dont know how to properly integrate them into my app. I am aware the access token is used to make subsequent requests and the refresh token is used to maintain token freshness but I dont know how to implement it with this stack.
mutations.py
Here is my login mutation that returns the access_token and refresh_token. It works fine.
#mutation.field("login")
#convert_kwargs_to_snake_case
def resolve_login(_, info, username, password):
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
access_token = create_access_token(identity=username)
refresh_token = create_refresh_token(identity=username)
payload = {
"user": user,
"access_token": access_token,
"refresh_token": refresh_token,
}
return payload
core.py
Here are my JWT configs, from what I have gathered online I am supposed to do a check on the token on each api request to maintain its freshness but I dont know how to do that especially with python + ariadne. Here is a link with someone implementing it with nodejs: https://github.com/benawad/graphql-express-template/blob/22_advanced_jwt_auth/auth.js
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://localhost/pinkle"
app.config["JWT_SECRET_KEY"] = "this_is_a_secret"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
JWTManager(app)
index.js
Here is my front end making the call to login the user, it returns the tokens but I dont know where to utilize the tokens or if I should save it in client side state and just make calls with the token.
function login({ username, password }) {
axios
.post('http://localhost:5000/graphql', {
query: `mutation {
login(username: "${username}", password: "${password}") {
user {
id
username
password
}
}
}`,
})
.then(result => {
console.log(result.data)
})
}
The Flask-JWT-Extended documentation includes examples of utilizing JWTs from JavaScript which might be helpful for you: https://flask-jwt-extended.readthedocs.io/en/stable/token_locations/
I am building an flask API that will manipulate data to be used in thingsboard.
Long story short, client logs in via thingsboard, get a jwt from that.
But the API rejects the jwt saying that signature verification failed.
Same secret key is being used on both sides.
Both thingsboard is on a remote server, API is currently local during development.
New to jwt manipulation, what am I missing?
Make sure that JWT_AUTH_USERNAME_KEY fit with the username that the library uses to get the user.
For example, when you get a Thingsboard JWT the username is in the sub key, and many libraries use username by default.
Maybe this piece of code can help you (implementation with JWT Rest Framework:
class CustomAuth(JSONWebTokenAuthentication):
def authenticate_credentials(self, payload):
"""
Returns an active user that matches the payload's user id and email.
"""
User = get_user_model()
# Thingsboard payload
if payload.get('sub') is not None:
username = payload.get('sub')
else:
# Our payload
username = payload.get('username')
if not username:
msg = _('Invalid payload.')
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist:
msg = _('Invalid signature.')
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = _('User account is disabled.')
raise exceptions.AuthenticationFailed(msg)
return user
Version 3.21.0 of flask-jwt-extended adds a JWT_DECODE_ISSUER option which should solve this for you: https://github.com/vimalloc/flask-jwt-extended/releases/tag/3.21.0
How to get 'jti', not mine but by the "username" ?
On registration/login jti for access and refresh were assigned to 'username'.
All instructions and docs note only how to get my own jti to logout with smth. like this code:
class UserLogoutRefresh(Resource):
#jwt_refresh_token_required
def post(self):
jti = get_raw_jwt()['jti']
try:
revoked_token = RevokedTokenModel(jti = jti)
revoked_token.add()
return {'message': 'Refresh token has been revoked'},200
except:
return {'message': 'Something went wrong'}, 500
How to get jti for another user.
I suppose smth like:
jti = get_raw_jwt(identity='someusername')['jti']
has to exist for this, who knows ???
Each token will have a unique jti, it’s not scoped per username. If the same user creates 3 tokens, all of them will have a different jti for example. If you want to have access to these jti then you will use the decode_token function when you create your token and save it in a datastore somewhere you can look it up later