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/
|
||||
|
||||
# Local agent workspaces
|
||||
/.agents/
|
||||
/.history/
|
||||
|
|
|
|||
|
|
@ -40,11 +40,21 @@ def verify_token(jwt_token):
|
|||
return abort(401)
|
||||
try:
|
||||
payload = jwt.decode(jwt_token, app.config["SECRET_KEY"], algorithms=["HS256"])
|
||||
username = payload["username"]
|
||||
the_user = app.specter.user_manager.get_user_by_username(username)
|
||||
if not the_user:
|
||||
username = payload.get("username")
|
||||
jwt_token_id = payload.get("jwt_token_id")
|
||||
if (
|
||||
not isinstance(username, str)
|
||||
or not username
|
||||
or not isinstance(jwt_token_id, str)
|
||||
or not jwt_token_id
|
||||
):
|
||||
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(f"Rest-Request for user {username} PASSED JWT-test")
|
||||
return username
|
||||
|
|
|
|||
|
|
@ -463,10 +463,10 @@ class User(UserMixin):
|
|||
|
||||
def verify_jwt_token_id_and_jwt_token(self, jwt_token_id, jwt_token):
|
||||
# Verifying the JWT token ID and JWT token
|
||||
if jwt_token_id in self.jwt_tokens:
|
||||
if self.jwt_tokens[jwt_token_id]["jwt_token"] == jwt_token:
|
||||
return True
|
||||
if not isinstance(self.jwt_tokens, dict) or not isinstance(jwt_token_id, str):
|
||||
return False
|
||||
token_info = self.jwt_tokens.get(jwt_token_id)
|
||||
return isinstance(token_info, dict) and token_info.get("jwt_token") == jwt_token
|
||||
|
||||
def get_jwt_token(self, jwt_token_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
|
||||
|
||||
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
|
||||
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)
|
||||
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
|
||||
response = client.get(
|
||||
"/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__)
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
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)
|
||||
headers = {
|
||||
"Authorization": "Bearer "
|
||||
+ User.generate_jwt_token(
|
||||
"admin", "tokenid", "tokendescription", random.randrange(100, 200)
|
||||
)
|
||||
+ create_registered_jwt_token(specter_regtest_configured, "admin")
|
||||
}
|
||||
result = client.get(
|
||||
"/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)
|
||||
headers = {
|
||||
"Authorization": "Bearer "
|
||||
+ User.generate_jwt_token(
|
||||
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
|
||||
)
|
||||
+ create_registered_jwt_token(specter_regtest_configured, "someuser")
|
||||
}
|
||||
result = client.get(
|
||||
"/api/v1alpha/wallets/a_simple_wallet/psbt",
|
||||
|
|
@ -97,9 +104,7 @@ def test_rr_psbt_post(specter_regtest_configured, bitcoin_regtest, client, caplo
|
|||
|
||||
headers = {
|
||||
"Authorization": "Bearer "
|
||||
+ User.generate_jwt_token(
|
||||
"someuser", "tokenid", "tokendescription", random.randrange(100, 200)
|
||||
),
|
||||
+ create_registered_jwt_token(specter_regtest_configured, "someuser"),
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue