mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
fix: validate active API tokens (#2685)
This commit is contained in:
parent
0b027f49fd
commit
48fea77b8a
5 changed files with 137 additions and 17 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -59,3 +59,7 @@ _bmad-output/
|
||||||
|
|
||||||
# Claude Code local workspace
|
# Claude Code local workspace
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# Local agent workspaces
|
||||||
|
/.agents/
|
||||||
|
/.history/
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,21 @@ def verify_token(jwt_token):
|
||||||
return abort(401)
|
return abort(401)
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(jwt_token, app.config["SECRET_KEY"], algorithms=["HS256"])
|
payload = jwt.decode(jwt_token, app.config["SECRET_KEY"], algorithms=["HS256"])
|
||||||
username = payload["username"]
|
username = payload.get("username")
|
||||||
the_user = app.specter.user_manager.get_user_by_username(username)
|
jwt_token_id = payload.get("jwt_token_id")
|
||||||
if not the_user:
|
if (
|
||||||
|
not isinstance(username, str)
|
||||||
|
or not username
|
||||||
|
or not isinstance(jwt_token_id, str)
|
||||||
|
or not jwt_token_id
|
||||||
|
):
|
||||||
return abort(401)
|
return abort(401)
|
||||||
g.user = app.specter.user_manager.get_user_by_username(username)
|
the_user = app.specter.user_manager.get_user_by_username(username)
|
||||||
|
if not the_user or not the_user.verify_jwt_token_id_and_jwt_token(
|
||||||
|
jwt_token_id, jwt_token
|
||||||
|
):
|
||||||
|
return abort(401)
|
||||||
|
g.user = the_user
|
||||||
logger.info({"payload": payload})
|
logger.info({"payload": payload})
|
||||||
logger.info(f"Rest-Request for user {username} PASSED JWT-test")
|
logger.info(f"Rest-Request for user {username} PASSED JWT-test")
|
||||||
return username
|
return username
|
||||||
|
|
|
||||||
|
|
@ -463,10 +463,10 @@ class User(UserMixin):
|
||||||
|
|
||||||
def verify_jwt_token_id_and_jwt_token(self, jwt_token_id, jwt_token):
|
def verify_jwt_token_id_and_jwt_token(self, jwt_token_id, jwt_token):
|
||||||
# Verifying the JWT token ID and JWT token
|
# Verifying the JWT token ID and JWT token
|
||||||
if jwt_token_id in self.jwt_tokens:
|
if not isinstance(self.jwt_tokens, dict) or not isinstance(jwt_token_id, str):
|
||||||
if self.jwt_tokens[jwt_token_id]["jwt_token"] == jwt_token:
|
return False
|
||||||
return True
|
token_info = self.jwt_tokens.get(jwt_token_id)
|
||||||
return False
|
return isinstance(token_info, dict) and token_info.get("jwt_token") == jwt_token
|
||||||
|
|
||||||
def get_jwt_token(self, jwt_token_id):
|
def get_jwt_token(self, jwt_token_id):
|
||||||
# Getting a JWT token from the hashmap by ID
|
# Getting a JWT token from the hashmap by ID
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,99 @@ def test_token_endpoints(client, empty_data_folder, caplog):
|
||||||
assert data["jwt_token_life"] == 360
|
assert data["jwt_token_life"] == 360
|
||||||
|
|
||||||
jwt_token_id = data["jwt_token_id"]
|
jwt_token_id = data["jwt_token_id"]
|
||||||
|
jwt_token = data["jwt_token"]
|
||||||
|
|
||||||
|
# API-created tokens remain registered after the user store is reloaded.
|
||||||
|
client.application.specter.user_manager.update()
|
||||||
|
|
||||||
|
# An active, registered token authenticates successfully. The missing wallet
|
||||||
|
# is rejected by authorization after authentication has completed.
|
||||||
|
token_headers = {"Authorization": "Bearer " + jwt_token}
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1alpha/wallets/missing/psbt",
|
||||||
|
follow_redirects=True,
|
||||||
|
headers=token_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
# A signed token must still be registered in the user's active token store.
|
||||||
|
unregistered_token = User.generate_jwt_token(
|
||||||
|
"someuser", User.generate_token_id(), "unregistered", 360
|
||||||
|
)
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1alpha/wallets/missing/psbt",
|
||||||
|
follow_redirects=True,
|
||||||
|
headers={"Authorization": "Bearer " + unregistered_token},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
# Missing and non-string token identifiers fail closed.
|
||||||
|
expiry = datetime.datetime.utcnow() + datetime.timedelta(seconds=360)
|
||||||
|
invalid_payloads = [
|
||||||
|
{"username": "someuser", "exp": expiry},
|
||||||
|
{"username": "someuser", "jwt_token_id": ["invalid"], "exp": expiry},
|
||||||
|
]
|
||||||
|
for invalid_payload in invalid_payloads:
|
||||||
|
invalid_token = jwt.encode(
|
||||||
|
invalid_payload,
|
||||||
|
client.application.config["SECRET_KEY"],
|
||||||
|
algorithm="HS256",
|
||||||
|
)
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1alpha/wallets/missing/psbt",
|
||||||
|
follow_redirects=True,
|
||||||
|
headers={"Authorization": "Bearer " + invalid_token},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
# A different signed token cannot borrow an active token's identifier.
|
||||||
|
mismatched_token = User.generate_jwt_token(
|
||||||
|
"someuser", jwt_token_id, "mismatched", 360
|
||||||
|
)
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1alpha/wallets/missing/psbt",
|
||||||
|
follow_redirects=True,
|
||||||
|
headers={"Authorization": "Bearer " + mismatched_token},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
# Malformed persisted records fail closed instead of raising an error.
|
||||||
|
user_details = client.application.specter.user_manager.get_user_by_username(
|
||||||
|
"someuser"
|
||||||
|
)
|
||||||
|
stored_token_info = user_details.jwt_tokens[jwt_token_id]
|
||||||
|
user_details.jwt_tokens[jwt_token_id] = {}
|
||||||
|
user_details.save_info()
|
||||||
|
client.application.specter.user_manager.update()
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1alpha/wallets/missing/psbt",
|
||||||
|
follow_redirects=True,
|
||||||
|
headers=token_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
user_details = client.application.specter.user_manager.get_user_by_username(
|
||||||
|
"someuser"
|
||||||
|
)
|
||||||
|
user_details.jwt_tokens[jwt_token_id] = stored_token_info
|
||||||
|
user_details.save_info()
|
||||||
|
|
||||||
|
# A malformed persisted token container also fails closed after reload.
|
||||||
|
stored_tokens = user_details.jwt_tokens
|
||||||
|
user_details.jwt_tokens = []
|
||||||
|
user_details.save_info()
|
||||||
|
client.application.specter.user_manager.update()
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1alpha/wallets/missing/psbt",
|
||||||
|
follow_redirects=True,
|
||||||
|
headers=token_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
user_details = client.application.specter.user_manager.get_user_by_username(
|
||||||
|
"someuser"
|
||||||
|
)
|
||||||
|
user_details.jwt_tokens = stored_tokens
|
||||||
|
user_details.save_info()
|
||||||
|
client.application.specter.user_manager.update()
|
||||||
|
|
||||||
# testing GET request
|
# testing GET request
|
||||||
response = client.get("/api/v1alpha/token", follow_redirects=True, headers=headers)
|
response = client.get("/api/v1alpha/token", follow_redirects=True, headers=headers)
|
||||||
|
|
@ -133,6 +226,14 @@ def test_token_endpoints(client, empty_data_folder, caplog):
|
||||||
data = json.loads(response.data)
|
data = json.loads(response.data)
|
||||||
assert data["message"] == "Token deleted"
|
assert data["message"] == "Token deleted"
|
||||||
|
|
||||||
|
# Deletion immediately revokes the bearer token.
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1alpha/wallets/missing/psbt",
|
||||||
|
follow_redirects=True,
|
||||||
|
headers=token_headers,
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
# retry accessing a deleted token
|
# retry accessing a deleted token
|
||||||
response = client.get(
|
response = client.get(
|
||||||
"/api/v1alpha/token/" + jwt_token_id, follow_redirects=True, headers=headers
|
"/api/v1alpha/token/" + jwt_token_id, follow_redirects=True, headers=headers
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,17 @@ from cryptoadvance.specter.user import User
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_registered_jwt_token(specter, username):
|
||||||
|
user = specter.user_manager.get_user_by_username(username)
|
||||||
|
jwt_token_id = User.generate_token_id()
|
||||||
|
jwt_token_life = 3600
|
||||||
|
jwt_token = User.generate_jwt_token(
|
||||||
|
username, jwt_token_id, "test token", jwt_token_life
|
||||||
|
)
|
||||||
|
user.add_jwt_token(jwt_token_id, jwt_token, "test token", jwt_token_life)
|
||||||
|
return jwt_token
|
||||||
|
|
||||||
|
|
||||||
def almost_equal(a: Number, b: Number, precision: float = 0.01) -> bool:
|
def almost_equal(a: Number, b: Number, precision: float = 0.01) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if a and b are not very different.
|
Checks if a and b are not very different.
|
||||||
|
|
@ -58,9 +69,7 @@ def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog
|
||||||
# Admin but not authorized (admin is NOT allowed to read everything)
|
# Admin but not authorized (admin is NOT allowed to read everything)
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer "
|
"Authorization": "Bearer "
|
||||||
+ User.generate_jwt_token(
|
+ create_registered_jwt_token(specter_regtest_configured, "admin")
|
||||||
"admin", "tokenid", "tokendescription", random.randrange(100, 200)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
result = client.get(
|
result = client.get(
|
||||||
"/api/v1alpha/wallets/a_simple_wallet/psbt",
|
"/api/v1alpha/wallets/a_simple_wallet/psbt",
|
||||||
|
|
@ -76,9 +85,7 @@ def test_rr_psbt_get(client, specter_regtest_configured, bitcoin_regtest, caplog
|
||||||
# Proper authorized (the wallet is owned by someuser)
|
# Proper authorized (the wallet is owned by someuser)
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer "
|
"Authorization": "Bearer "
|
||||||
+ User.generate_jwt_token(
|
+ create_registered_jwt_token(specter_regtest_configured, "someuser")
|
||||||
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
result = client.get(
|
result = client.get(
|
||||||
"/api/v1alpha/wallets/a_simple_wallet/psbt",
|
"/api/v1alpha/wallets/a_simple_wallet/psbt",
|
||||||
|
|
@ -97,9 +104,7 @@ def test_rr_psbt_post(specter_regtest_configured, bitcoin_regtest, client, caplo
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer "
|
"Authorization": "Bearer "
|
||||||
+ User.generate_jwt_token(
|
+ create_registered_jwt_token(specter_regtest_configured, "someuser"),
|
||||||
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
|
|
||||||
),
|
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue