0

My API allows a user to update their user details by with a PUT request to the /User endpoint.

However, when I try to send an update. It is failing when trying to save the password with the exception Expected Bytes.

Below is my route:

@UserNS.expect(UserModel)
    @token_required
    def put(self):
        """Updates the user details associated with the API token."""
        token = request.headers['Token']
        data = request.get_json()
        if not data:
            return jsonify({'message': 'Please enter some user data'})
        else:
            update_user = User.query.filter_by(api_token=token).first()
            print("Updating user details")
            print("Current: " + update_user.username + " " + update_user.email)
            print("New: " + data["Username"] + " " + data["Email Address"])
            try:
                update_user.username = str(data["Username"])
                update_user.email = str(data['Email Address'])
                print("Trying to save password...")
                update_user.set_password(data['Password'])
                print("uh oh...")
                db.session.commit()
                return {'Message': 'Record updated!'}
            except Exception as e:
                print(e)
                return {'Message': 'Username or email address is invalid!'}

Below is my API model:

UserModel = api.model('User', {'Username': fields.String(), 'Email Address': fields.String(), 'Password': fields.String()})

And my SQLAlchemy model:

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True, unique=True)
    api_token = db.Column(db.String(50), unique=True)
    username = db.Column(db.String(128), index=True, unique=True)
    email = db.Column(db.String(128), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    todos = db.relationship('Todo', backref='owner', lazy='dynamic')

I am using very similar data to register the account. So not sure wht it is not saving the new data.

Edit: I think the problem is Python is converting the update_user to a non bytes object here:

update_user = User.query.filter_by(api_token=token).first()

So I need a way to read to make the query and store it for manipulation. Without transforming it to a non-bytes object.

edit2:

Flask-login is being used to store the password using the below definition:

def set_password(self, password):
    self.password_hash = generate_password_hash(password)
Keva161
  • 2,623
  • 9
  • 44
  • 68
  • Is it getting that exception at the commit() call or earlier? What frameworks are you using? I don't think the way you are reading the User object is the problem. – totalhack Feb 09 '20 at 14:41
  • The print statements from the console are: `Updating user details` `Current: Cindy sylvialopez@yahoo.com` `New: Ethan derek07@gmail.com` `Trying to save password...` `Expected bytes` `127.0.0.1 - - [09/Feb/2020 14:51:42] "PUT /api/Users/ HTTP/1.1" 200 -` I'm using flask and flask rest plus for the API. – Keva161 Feb 09 '20 at 14:52
  • Is it Flask Login? What does set_password look like? – totalhack Feb 09 '20 at 14:55
  • It is yes. Updated the original post. – Keva161 Feb 09 '20 at 15:04
  • Have you tried converting `data['Password']` to bytes before passing it in to set_password at least to confirm that works around the issue (if its a `str` try encoding it as utf8 perhaps)? The tutorials on this stuff generally show taking data directly from a form object and calling a set_password method that generates the password hash. Perhaps get_json() is converting the password to a `str` and you then need to convert it back to bytes. See example further down [here](https://exploreflask.com/en/latest/users.html#forgot-your-password) where they use a form object. – totalhack Feb 09 '20 at 15:04
  • I assume you are using Python 3 btw. I just tried using `werkzeug.security.generate_password_hash` directly, which is what I expect is being used here, and it accepts a `str` without error. Perhaps a stacktrace would be helpful. – totalhack Feb 09 '20 at 15:14
  • I just had a look an the object that the server tried to save when it fails is an `int`. Converting it to a string with `str()` seems to successfully save. – Keva161 Feb 09 '20 at 15:14
  • So data['Password'] was an int? – totalhack Feb 09 '20 at 15:16
  • Yes. My API test was generating an int. – Keva161 Feb 09 '20 at 15:20

1 Answers1

1

The underlying werkzeug.security.generate_password_hash method expects a str as input. If you pass it an invalid type such as an int, you get the TypeError: Expected bytes exception. Method docs here.

As noted in my comment above, there are tutorials that show working examples that ensure the proper input types on the form.

totalhack
  • 2,298
  • 17
  • 23