Skip to content

Commit 450003c

Browse files
committed
Reject retired API credentials before use
1 parent 929a1b5 commit 450003c

5 files changed

Lines changed: 144 additions & 9 deletions

File tree

‎socketdev/__init__.py‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22
from socketdev.core.api import API
3+
from socketdev.core.retired_token import reject_retired_token
4+
from socketdev.core.token_selection import select_token
35
from socketdev.dependencies import Dependencies
46
from socketdev.diffscans import DiffScans
57
from socketdev.export import Export
@@ -53,21 +55,15 @@
5355

5456
class socketdev:
5557
def __init__(self, token: Optional[str] = None, timeout: int = 1200, allow_unverified: bool = False, user_agent: Optional[str] = None):
56-
# Try to get token from environment variables if not provided
57-
if token is None:
58-
token = (
59-
os.getenv("SOCKET_SECURITY_API_TOKEN") or
60-
os.getenv("SOCKET_SECURITY_API_KEY") or
61-
os.getenv("SOCKET_API_KEY") or
62-
os.getenv("SOCKET_API_TOKEN")
63-
)
64-
58+
token = select_token(token, os.environ)
6559
if token is None:
6660
raise ValueError(
6761
"API token is required. Provide it as a parameter or set one of these environment variables: "
6862
"SOCKET_SECURITY_API_TOKEN, SOCKET_SECURITY_API_KEY, SOCKET_API_KEY, SOCKET_API_TOKEN"
6963
)
7064

65+
reject_retired_token(token)
66+
7167
self.api = API()
7268
self.token = token + ":"
7369
self.api.encode_key(self.token)

‎socketdev/core/api.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
from socketdev.log import log
3+
from socketdev.core.retired_token import reject_retired_basic_auth_token
34

45
import requests
56
from socketdev.core.classes import Response
@@ -29,6 +30,7 @@ def __init__(self):
2930
self.user_agent = None
3031

3132
def encode_key(self, token: str):
33+
reject_retired_basic_auth_token(token)
3234
self.encoded_key = base64.b64encode(token.encode()).decode("ascii")
3335

3436
def set_timeout(self, timeout: int):

‎socketdev/core/retired_token.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import hashlib
2+
import hmac
3+
from typing import Optional
4+
5+
RETIRED_TOKEN_SHA256 = "9cab58b24140ee12c08e265729d246f9200f8ef9b1d884f90c06c4e6b69adf18"
6+
RETIRED_TOKEN_ERROR = (
7+
"This API token has been retired. Create a new API token in Socket "
8+
"and update your configuration."
9+
)
10+
11+
12+
def _matches_retired_digest(token: Optional[str], digest: str) -> bool:
13+
if token is None:
14+
return False
15+
token_digest = hashlib.sha256(token.strip().encode("utf-8")).hexdigest()
16+
return hmac.compare_digest(token_digest, digest)
17+
18+
19+
def _reject_retired_token(token: Optional[str], digest: str) -> None:
20+
if _matches_retired_digest(token, digest):
21+
raise ValueError(RETIRED_TOKEN_ERROR)
22+
23+
24+
def _reject_basic_auth_token(token: str, digest: str) -> None:
25+
_reject_retired_token(token.partition(":")[0], digest)
26+
27+
28+
def reject_retired_token(token: Optional[str]) -> None:
29+
_reject_retired_token(token, RETIRED_TOKEN_SHA256)
30+
31+
32+
def reject_retired_basic_auth_token(token: str) -> None:
33+
_reject_basic_auth_token(token, RETIRED_TOKEN_SHA256)

‎socketdev/core/token_selection.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import Mapping, Optional
2+
3+
TOKEN_ENVIRONMENT_VARIABLES = (
4+
"SOCKET_SECURITY_API_TOKEN",
5+
"SOCKET_SECURITY_API_KEY",
6+
"SOCKET_API_KEY",
7+
"SOCKET_API_TOKEN",
8+
)
9+
10+
11+
def select_token(
12+
explicit_token: Optional[str], environment: Mapping[str, str]
13+
) -> Optional[str]:
14+
if explicit_token is not None:
15+
return explicit_token
16+
return next(
17+
(environment[name] for name in TOKEN_ENVIRONMENT_VARIABLES if environment.get(name)),
18+
None,
19+
)

‎tests/unit/test_retired_token.py‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import base64
2+
import hashlib
3+
4+
import pytest
5+
6+
from socketdev import socketdev
7+
from socketdev.core.api import API
8+
from socketdev.core.retired_token import (
9+
RETIRED_TOKEN_ERROR,
10+
_reject_basic_auth_token,
11+
_reject_retired_token,
12+
)
13+
from socketdev.core.token_selection import TOKEN_ENVIRONMENT_VARIABLES, select_token
14+
15+
16+
def test_retired_token_rejection_redacts_token_and_digest():
17+
token = "synthetic-retired-token"
18+
digest = hashlib.sha256(token.encode()).hexdigest()
19+
20+
with pytest.raises(ValueError) as exc_info:
21+
_reject_retired_token(token, digest)
22+
23+
assert str(exc_info.value) == RETIRED_TOKEN_ERROR
24+
assert token not in str(exc_info.value)
25+
assert digest not in str(exc_info.value)
26+
27+
28+
def test_sdk_constructor_accepts_safe_explicit_token_without_network_access():
29+
sdk = socketdev(token="synthetic-safe-token")
30+
31+
assert sdk.token == "synthetic-safe-token:"
32+
33+
34+
def test_api_encode_key_preserves_each_input_byte_sequence():
35+
bare_api = API()
36+
suffixed_api = API()
37+
bare_token = "synthetic-safe-token"
38+
suffixed_token = "synthetic-safe-token:"
39+
40+
bare_api.encode_key(bare_token)
41+
suffixed_api.encode_key(suffixed_token)
42+
43+
assert bare_api.encoded_key == base64.b64encode(bare_token.encode()).decode("ascii")
44+
assert suffixed_api.encoded_key == base64.b64encode(suffixed_token.encode()).decode("ascii")
45+
46+
47+
def test_api_rejects_retired_basic_username_with_password_suffix():
48+
token = "synthetic-retired-token"
49+
digest = hashlib.sha256(token.encode()).hexdigest()
50+
51+
with pytest.raises(ValueError, match="token has been retired"):
52+
_reject_basic_auth_token(f"{token}:password", digest)
53+
54+
55+
def test_token_selection_preserves_explicit_and_environment_precedence():
56+
environment = {
57+
name: f"token-{index}"
58+
for index, name in enumerate(TOKEN_ENVIRONMENT_VARIABLES)
59+
}
60+
61+
assert select_token("", environment) == ""
62+
assert select_token("explicit", environment) == "explicit"
63+
assert select_token(None, environment) == "token-0"
64+
assert select_token(
65+
None,
66+
{TOKEN_ENVIRONMENT_VARIABLES[0]: "", TOKEN_ENVIRONMENT_VARIABLES[1]: "next"},
67+
) == "next"
68+
assert select_token(None, {}) is None
69+
70+
71+
@pytest.mark.parametrize("token", [None, "different-token"])
72+
def test_nonretired_token_is_accepted(token):
73+
digest = hashlib.sha256(b"synthetic-retired-token").hexdigest()
74+
_reject_retired_token(token, digest)
75+
76+
77+
def test_retired_token_comparison_trims_whitespace():
78+
digest = hashlib.sha256(b"synthetic-retired-token").hexdigest()
79+
with pytest.raises(ValueError):
80+
_reject_retired_token(" \tsynthetic-retired-token\n", digest)
81+
82+
83+
@pytest.mark.parametrize("name", TOKEN_ENVIRONMENT_VARIABLES)
84+
def test_each_environment_source_is_supported(name):
85+
assert select_token(None, {name: "selected-token"}) == "selected-token"

0 commit comments

Comments
 (0)