Skip to content

Commit f4e8835

Browse files
committed
feat(auth): add OAuth token revocation (RFC 7009)
auth.oauth.revoke(token, client_id, token_type_hint=None) POSTs /auth/v1/oauth/revoke, the endpoint campus-cli still raw-calls. The server returns 200 {} regardless of token state (RFC 7009 §2.2); errors (missing token, invalid client) surface as APIError subclasses via raise_for_status. Fixes #73
1 parent 9a04cb5 commit f4e8835

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

‎campus_python/auth/v1/oauth.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,30 @@ def authorize_device(
158158
)
159159
resp.raise_for_status()
160160
return resp.json()
161+
162+
def revoke(
163+
self,
164+
token: str,
165+
client_id: str,
166+
token_type_hint: "str | None" = None,
167+
) -> None:
168+
"""Revoke an access or refresh token (RFC 7009).
169+
170+
Args:
171+
token: The access or refresh token to revoke
172+
client_id: The OAuth client the token was issued to
173+
token_type_hint: Optional "access_token" or "refresh_token"
174+
175+
Per RFC 7009 section 2.2 the server returns 200 regardless of
176+
whether the token was found or already revoked, so callers
177+
cannot use this endpoint to confirm a token's validity.
178+
"""
179+
json_body: dict = {
180+
"token": token,
181+
"client_id": client_id,
182+
}
183+
if token_type_hint is not None:
184+
json_body["token_type_hint"] = token_type_hint
185+
resp = self.client.post(self.make_path("revoke"), json=json_body)
186+
resp.raise_for_status()
187+
return None

‎tests/unit/test_oauth_revoke.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Contract tests for OAuth token revocation (RFC 7009, issue #73).
2+
3+
The device flow moved in-library with the PR #70 fix sweep, but
4+
campus-cli still raw-calls POST /auth/v1/oauth/revoke. The endpoint is
5+
registered as the leaf path /oauth/revoke (campus/auth/routes/
6+
oauth.py; the auth app sets strict_slashes) and returns 200 {} per
7+
RFC 7009 section 2.2 regardless of token state.
8+
"""
9+
10+
import unittest
11+
from unittest.mock import Mock
12+
13+
from campus_python.auth.v1 import AuthRoot
14+
15+
16+
def make_auth() -> tuple[AuthRoot, Mock]:
17+
"""Create an AuthRoot backed by a mock JSON client."""
18+
client = Mock()
19+
return AuthRoot(json_client=client), client
20+
21+
22+
class TestOAuthRevoke(unittest.TestCase):
23+
"""auth.oauth.revoke() must POST the /oauth/revoke leaf route."""
24+
25+
def setUp(self):
26+
self.auth, self.client = make_auth()
27+
28+
def test_revoke_posts_token_and_client_id(self):
29+
self.auth.oauth.revoke(token="tok-1", client_id="campus-cli")
30+
self.client.post.assert_called_once_with(
31+
"/auth/v1/oauth/revoke",
32+
json={"token": "tok-1", "client_id": "campus-cli"},
33+
)
34+
35+
def test_revoke_sends_token_type_hint_when_given(self):
36+
self.auth.oauth.revoke(
37+
token="tok-1",
38+
client_id="campus-cli",
39+
token_type_hint="refresh_token",
40+
)
41+
self.assertEqual(
42+
self.client.post.call_args.kwargs["json"],
43+
{
44+
"token": "tok-1",
45+
"client_id": "campus-cli",
46+
"token_type_hint": "refresh_token",
47+
},
48+
)
49+
50+
51+
if __name__ == "__main__":
52+
unittest.main()

0 commit comments

Comments
 (0)