Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions campus_python/auth/v1/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,30 @@ def authorize_device(
)
resp.raise_for_status()
return resp.json()

def revoke(
self,
token: str,
client_id: str,
token_type_hint: "str | None" = None,
) -> None:
"""Revoke an access or refresh token (RFC 7009).

Args:
token: The access or refresh token to revoke
client_id: The OAuth client the token was issued to
token_type_hint: Optional "access_token" or "refresh_token"

Per RFC 7009 section 2.2 the server returns 200 regardless of
whether the token was found or already revoked, so callers
cannot use this endpoint to confirm a token's validity.
"""
json_body: dict = {
"token": token,
"client_id": client_id,
}
if token_type_hint is not None:
json_body["token_type_hint"] = token_type_hint
resp = self.client.post(self.make_path("revoke"), json=json_body)
resp.raise_for_status()
return None
52 changes: 52 additions & 0 deletions tests/unit/test_oauth_revoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Contract tests for OAuth token revocation (RFC 7009, issue #73).

The device flow moved in-library with the PR #70 fix sweep, but
campus-cli still raw-calls POST /auth/v1/oauth/revoke. The endpoint is
registered as the leaf path /oauth/revoke (campus/auth/routes/
oauth.py; the auth app sets strict_slashes) and returns 200 {} per
RFC 7009 section 2.2 regardless of token state.
"""

import unittest
from unittest.mock import Mock

from campus_python.auth.v1 import AuthRoot


def make_auth() -> tuple[AuthRoot, Mock]:
"""Create an AuthRoot backed by a mock JSON client."""
client = Mock()
return AuthRoot(json_client=client), client


class TestOAuthRevoke(unittest.TestCase):
"""auth.oauth.revoke() must POST the /oauth/revoke leaf route."""

def setUp(self):
self.auth, self.client = make_auth()

def test_revoke_posts_token_and_client_id(self):
self.auth.oauth.revoke(token="tok-1", client_id="campus-cli")
self.client.post.assert_called_once_with(
"/auth/v1/oauth/revoke",
json={"token": "tok-1", "client_id": "campus-cli"},
)

def test_revoke_sends_token_type_hint_when_given(self):
self.auth.oauth.revoke(
token="tok-1",
client_id="campus-cli",
token_type_hint="refresh_token",
)
self.assertEqual(
self.client.post.call_args.kwargs["json"],
{
"token": "tok-1",
"client_id": "campus-cli",
"token_type_hint": "refresh_token",
},
)


if __name__ == "__main__":
unittest.main()
Loading