Compare commits
4 Commits
3d93e489c4
...
3599e80e77
Author | SHA1 | Date | |
---|---|---|---|
3599e80e77 | |||
ca0c6512fe | |||
0bd36b9a95 | |||
2d56823b17 |
@ -28,6 +28,12 @@ The server will respond like this:
|
|||||||
"username": "user"
|
"username": "user"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Ensure that you are indeed using ``POST``.
|
||||||
|
Otherwise, you are querying the user with the name ``login``.
|
||||||
|
This will result in a "not authorized" error.
|
||||||
|
|
||||||
Save the token in ``auth_token``.
|
Save the token in ``auth_token``.
|
||||||
|
|
||||||
.. _authentication_usage:
|
.. _authentication_usage:
|
||||||
@ -69,7 +75,7 @@ You can now use the new token in ``auth_token`` for future authentication.
|
|||||||
This does not revoke your old token.
|
This does not revoke your old token.
|
||||||
See :ref:`authentication_log_out` for information on revoking tokens.
|
See :ref:`authentication_log_out` for information on revoking tokens.
|
||||||
|
|
||||||
.. note::
|
.. warning::
|
||||||
Remember to use the ``POST`` HTTP method and not ``GET``.
|
Remember to use the ``POST`` HTTP method and not ``GET``.
|
||||||
If you use ``GET`` by accident, the server will assume you're trying to read the information of a user called 'extend'.
|
If you use ``GET`` by accident, the server will assume you're trying to read the information of a user called 'extend'.
|
||||||
This will result in a "not authorized" error.
|
This will result in a "not authorized" error.
|
||||||
@ -87,3 +93,33 @@ Use the following request body:
|
|||||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODUwNTk3NjIsImlhdCI6MTY4NDQ1NDk2Miwic3ViIjoidXNlciIsImp0aSI6InJlbmV3In0.ZITIK8L5FzLtm-ASwIf6TkTb69z4bsZ8FF0mWee4YI4"
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2ODUwNTk3NjIsImlhdCI6MTY4NDQ1NDk2Miwic3ViIjoidXNlciIsImp0aSI6InJlbmV3In0.ZITIK8L5FzLtm-ASwIf6TkTb69z4bsZ8FF0mWee4YI4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Ensure that you are indeed using ``POST``.
|
||||||
|
Otherwise, you are querying the user with the name ``logout``.
|
||||||
|
This will result in a "not authorized" error.
|
||||||
|
|
||||||
|
.. _authentication_password_change:
|
||||||
|
|
||||||
|
Password change
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Administrators can change a user's password via the ``PATCH/PUT /users/<username>`` endpoint.
|
||||||
|
See :ref:`user_info_api`.
|
||||||
|
|
||||||
|
A user can change their own password via the password change API::
|
||||||
|
|
||||||
|
POST /users/password
|
||||||
|
|
||||||
|
Use the following request body:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"old": "old_password",
|
||||||
|
"new": "new_password"
|
||||||
|
}
|
||||||
|
|
||||||
|
Send the user's current password in ``old``, and Sachet will change it to the password in ``new``.
|
||||||
|
@ -87,13 +87,17 @@ class User(db.Model):
|
|||||||
permissions.AllFlags = Permissions
|
permissions.AllFlags = Permissions
|
||||||
self.permissions = permissions
|
self.permissions = permissions
|
||||||
|
|
||||||
self.password = bcrypt.generate_password_hash(
|
self.password = self.gen_hash(password)
|
||||||
password, current_app.config.get("BCRYPT_LOG_ROUNDS")
|
|
||||||
).decode()
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.url = url_for("users_blueprint.user_api", username=self.username)
|
self.url = url_for("users_blueprint.user_api", username=self.username)
|
||||||
self.register_date = datetime.datetime.now()
|
self.register_date = datetime.datetime.now()
|
||||||
|
|
||||||
|
def gen_hash(self, psswd):
|
||||||
|
"""Generates a hash from a password."""
|
||||||
|
return bcrypt.generate_password_hash(
|
||||||
|
psswd, current_app.config.get("BCRYPT_LOG_ROUNDS")
|
||||||
|
).decode()
|
||||||
|
|
||||||
def encode_token(self, jti=None):
|
def encode_token(self, jti=None):
|
||||||
"""Generates an authentication token"""
|
"""Generates an authentication token"""
|
||||||
payload = {
|
payload = {
|
||||||
|
@ -3,12 +3,6 @@ from sachet.server.models import User
|
|||||||
|
|
||||||
|
|
||||||
def create_user(permissions, username, password):
|
def create_user(permissions, username, password):
|
||||||
# to reduce confusion with API endpoints
|
|
||||||
forbidden = {"login", "logout", "extend"}
|
|
||||||
|
|
||||||
if username in forbidden:
|
|
||||||
raise KeyError(f"Username '{username}' is reserved and can not be used.")
|
|
||||||
|
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
if not user:
|
if not user:
|
||||||
user = User(username=username, password=password, permissions=permissions)
|
user = User(username=username, password=password, permissions=permissions)
|
||||||
|
@ -8,6 +8,7 @@ from sachet.server.models import (
|
|||||||
)
|
)
|
||||||
from sachet.server.views_common import ModelAPI, ModelListAPI, auth_required
|
from sachet.server.views_common import ModelAPI, ModelListAPI, auth_required
|
||||||
from sachet.server import bcrypt, db
|
from sachet.server import bcrypt, db
|
||||||
|
import uuid
|
||||||
|
|
||||||
users_blueprint = Blueprint("users_blueprint", __name__)
|
users_blueprint = Blueprint("users_blueprint", __name__)
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ class LoginAPI(MethodView):
|
|||||||
return jsonify(resp), 401
|
return jsonify(resp), 401
|
||||||
|
|
||||||
if bcrypt.check_password_hash(user.password, post_data.get("password", "")):
|
if bcrypt.check_password_hash(user.password, post_data.get("password", "")):
|
||||||
token = user.encode_token()
|
token = user.encode_token(jti=f"login_{uuid.uuid4()}")
|
||||||
resp = {
|
resp = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": "Logged in.",
|
"message": "Logged in.",
|
||||||
@ -88,12 +89,58 @@ users_blueprint.add_url_rule(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordAPI(MethodView):
|
||||||
|
"""Endpoint to change passwords."""
|
||||||
|
|
||||||
|
@auth_required
|
||||||
|
def post(self, auth_user=None):
|
||||||
|
post_data = request.get_json()
|
||||||
|
old_psswd = post_data.get("old")
|
||||||
|
new_psswd = post_data.get("new")
|
||||||
|
|
||||||
|
if not old_psswd or not new_psswd:
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"message": "Specify the 'old' password and the 'new' password.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not bcrypt.check_password_hash(auth_user.password, old_psswd):
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"message": "Invalid 'old' password.",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
auth_user.password = auth_user.gen_hash(new_psswd)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Password changed.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
users_blueprint.add_url_rule(
|
||||||
|
"/users/password", view_func=PasswordAPI.as_view("password_api"), methods=["POST"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExtendAPI(MethodView):
|
class ExtendAPI(MethodView):
|
||||||
"""Endpoint to take a token and get a new one with a later expiry date."""
|
"""Endpoint to take a token and get a new one with a later expiry date."""
|
||||||
|
|
||||||
@auth_required
|
@auth_required
|
||||||
def post(self, auth_user=None):
|
def post(self, auth_user=None):
|
||||||
token = auth_user.encode_token(jti="renew")
|
token = auth_user.encode_token(jti=f"renew{uuid.uuid4()}")
|
||||||
resp = {
|
resp = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": "Renewed token.",
|
"message": "Renewed token.",
|
||||||
|
@ -4,13 +4,6 @@ from sachet.server import db
|
|||||||
from sachet.server.users import manage
|
from sachet.server.users import manage
|
||||||
|
|
||||||
|
|
||||||
def test_reserved_users(client):
|
|
||||||
"""Test that the server prevents reserved endpoints from being registered as usernames."""
|
|
||||||
for user in ["login", "logout", "extend"]:
|
|
||||||
with pytest.raises(KeyError):
|
|
||||||
manage.create_user(False, user, "")
|
|
||||||
|
|
||||||
|
|
||||||
def test_unauth_perms(client):
|
def test_unauth_perms(client):
|
||||||
"""Test endpoints to see if they allow unauthenticated users."""
|
"""Test endpoints to see if they allow unauthenticated users."""
|
||||||
resp = client.get("/users/jeff")
|
resp = client.get("/users/jeff")
|
||||||
@ -144,6 +137,47 @@ def test_logout(client, tokens, validate_info, auth):
|
|||||||
assert resp.status_code == 401
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_change(client, tokens, users, auth):
|
||||||
|
"""Test changing passwords."""
|
||||||
|
|
||||||
|
# test that we're logged in
|
||||||
|
resp = client.get("/users/jeff", headers=auth("jeff"))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# change password
|
||||||
|
resp = client.post(
|
||||||
|
"/users/password",
|
||||||
|
json=dict(old=users["jeff"]["password"], new="new_password"),
|
||||||
|
headers=auth("jeff"),
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# revoke old token
|
||||||
|
resp = client.post(
|
||||||
|
"/users/logout", json=dict(token=tokens["jeff"]), headers=auth("jeff")
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# test that we're logged out
|
||||||
|
resp = client.get(
|
||||||
|
"/users/jeff", headers=auth("jeff"), json=dict(token=tokens["jeff"])
|
||||||
|
)
|
||||||
|
assert resp.status_code == 401
|
||||||
|
|
||||||
|
# sign in with new token
|
||||||
|
resp = client.post(
|
||||||
|
"/users/login", json=dict(username="jeff", password="new_password")
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.get_json()
|
||||||
|
new_token = data.get("auth_token")
|
||||||
|
assert new_token
|
||||||
|
|
||||||
|
# test that we're logged in
|
||||||
|
resp = client.get("/users/jeff", headers=dict(Authorization=f"bearer {new_token}"))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_admin_revoke(client, tokens, validate_info, auth):
|
def test_admin_revoke(client, tokens, validate_info, auth):
|
||||||
"""Test that an admin can revoke any token from other users."""
|
"""Test that an admin can revoke any token from other users."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user