fix(claude): read credentials from ~/.claude/.credentials.json
The actual OAuth token is in ~/.claude/.credentials.json under claudeAiOauth.accessToken, not in ~/.claude.json. ~/.claude.json holds only UI state and profile metadata (oauthAccount has no token fields). expiresAt in the credentials file is milliseconds, not seconds. Discovered after testing against Claude Code 2.1.198. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,9 @@ class TestClaudeHostAccessToken(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory(prefix="bb-claude-auth.")
|
||||
self.home = Path(self.tmp.name)
|
||||
self.auth_path = self.home / ".claude.json"
|
||||
self.cred_dir = self.home / ".claude"
|
||||
self.cred_dir.mkdir()
|
||||
self.auth_path = self.cred_dir / ".credentials.json"
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
@@ -33,36 +35,40 @@ class TestClaudeHostAccessToken(unittest.TestCase):
|
||||
claude_auth_path({"HOME": str(self.home)}),
|
||||
)
|
||||
|
||||
def test_returns_session_key(self):
|
||||
def test_returns_access_token(self):
|
||||
key = "sk-ant-oat01-real-key"
|
||||
self._write({"oauthAccount": {"sessionKey": key}})
|
||||
self._write({"claudeAiOauth": {"accessToken": key}})
|
||||
out = claude_host_access_token({"HOME": str(self.home)})
|
||||
self.assertEqual(key, out)
|
||||
|
||||
def test_missing_auth_file_dies(self):
|
||||
with self.assertRaises(Die):
|
||||
claude_host_access_token({"HOME": str(self.home)})
|
||||
# Use a home with no .credentials.json
|
||||
empty_home = self.home / "empty"
|
||||
empty_home.mkdir()
|
||||
claude_host_access_token({"HOME": str(empty_home)})
|
||||
|
||||
def test_missing_oauth_account_dies(self):
|
||||
def test_missing_claude_ai_oauth_dies(self):
|
||||
self._write({"hasCompletedOnboarding": True})
|
||||
with self.assertRaises(Die):
|
||||
claude_host_access_token({"HOME": str(self.home)})
|
||||
|
||||
def test_missing_session_key_dies(self):
|
||||
self._write({"oauthAccount": {"expiresAt": 2000000000}})
|
||||
def test_missing_access_token_dies(self):
|
||||
self._write({"claudeAiOauth": {"expiresAt": 2000000000000}})
|
||||
with self.assertRaises(Die):
|
||||
claude_host_access_token({"HOME": str(self.home)})
|
||||
|
||||
def test_empty_session_key_dies(self):
|
||||
self._write({"oauthAccount": {"sessionKey": ""}})
|
||||
def test_empty_access_token_dies(self):
|
||||
self._write({"claudeAiOauth": {"accessToken": ""}})
|
||||
with self.assertRaises(Die):
|
||||
claude_host_access_token({"HOME": str(self.home)})
|
||||
|
||||
def test_expired_token_dies(self):
|
||||
# expiresAt is in milliseconds; 1000000 ms = year 1970
|
||||
self._write({
|
||||
"oauthAccount": {
|
||||
"sessionKey": "sk-ant-oat01-x",
|
||||
"expiresAt": 1000,
|
||||
"claudeAiOauth": {
|
||||
"accessToken": "sk-ant-oat01-x",
|
||||
"expiresAt": 1000000,
|
||||
},
|
||||
})
|
||||
with self.assertRaises(Die):
|
||||
@@ -73,10 +79,11 @@ class TestClaudeHostAccessToken(unittest.TestCase):
|
||||
|
||||
def test_future_expiry_is_accepted(self):
|
||||
key = "sk-ant-oat01-y"
|
||||
# 2000000000000 ms = ~year 2033
|
||||
self._write({
|
||||
"oauthAccount": {
|
||||
"sessionKey": key,
|
||||
"expiresAt": 2000000000,
|
||||
"claudeAiOauth": {
|
||||
"accessToken": key,
|
||||
"expiresAt": 2000000000000,
|
||||
},
|
||||
})
|
||||
out = claude_host_access_token(
|
||||
@@ -87,7 +94,7 @@ class TestClaudeHostAccessToken(unittest.TestCase):
|
||||
|
||||
def test_absent_expiry_field_is_accepted(self):
|
||||
key = "sk-ant-oat01-z"
|
||||
self._write({"oauthAccount": {"sessionKey": key}})
|
||||
self._write({"claudeAiOauth": {"accessToken": key}})
|
||||
out = claude_host_access_token({"HOME": str(self.home)})
|
||||
self.assertEqual(key, out)
|
||||
|
||||
@@ -101,6 +108,19 @@ class TestClaudeHostAccessToken(unittest.TestCase):
|
||||
with self.assertRaises(Die):
|
||||
claude_host_access_token({"HOME": str(self.home)})
|
||||
|
||||
def test_extra_fields_in_credentials_are_ignored(self):
|
||||
key = "sk-ant-oat01-real"
|
||||
self._write({
|
||||
"claudeAiOauth": {
|
||||
"accessToken": key,
|
||||
"refreshToken": "sk-ant-ort01-secret",
|
||||
"scopes": ["user:inference", "user:profile"],
|
||||
"expiresAt": 2000000000000,
|
||||
},
|
||||
})
|
||||
out = claude_host_access_token({"HOME": str(self.home)})
|
||||
self.assertEqual(key, out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user