gopher-mcp-python 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/PKG-INFO +1 -1
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/__init__.py +27 -1
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/ffi/__init__.py +8 -1
- gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/__init__.py +82 -0
- gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/auth_client.py +547 -0
- gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/loader.py +404 -0
- gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/types.py +135 -0
- gopher_mcp_python-0.1.2/gopher_mcp_python/ffi/auth/validation_options.py +239 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/PKG-INFO +1 -1
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/SOURCES.txt +5 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/pyproject.toml +1 -1
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/LICENSE +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/README.md +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/agent.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/config.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/errors.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/ffi/library.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/result.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python/server_config.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/dependency_links.txt +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/requires.txt +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/gopher_mcp_python.egg-info/top_level.txt +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/setup.cfg +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/setup.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/tests/test_config.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/tests/test_ffi.py +0 -0
- {gopher_mcp_python-0.1.1 → gopher_mcp_python-0.1.2}/tests/test_result.py +0 -0
|
@@ -29,7 +29,23 @@ from gopher_mcp_python.errors import AgentError, ApiKeyError, ConnectionError, T
|
|
|
29
29
|
from gopher_mcp_python.server_config import ServerConfig
|
|
30
30
|
from gopher_mcp_python.ffi import GopherOrchLibrary
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# Auth module re-exports
|
|
33
|
+
from gopher_mcp_python.ffi.auth import (
|
|
34
|
+
# Types
|
|
35
|
+
GopherAuthError,
|
|
36
|
+
ValidationResult,
|
|
37
|
+
TokenPayload,
|
|
38
|
+
GopherAuthContext,
|
|
39
|
+
# Classes
|
|
40
|
+
GopherAuthClient,
|
|
41
|
+
GopherValidationOptions,
|
|
42
|
+
# Functions
|
|
43
|
+
gopher_init_auth_library,
|
|
44
|
+
gopher_shutdown_auth_library,
|
|
45
|
+
is_auth_available,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__version__ = "0.1.2"
|
|
33
49
|
|
|
34
50
|
__all__ = [
|
|
35
51
|
# Main classes
|
|
@@ -47,6 +63,16 @@ __all__ = [
|
|
|
47
63
|
"TimeoutError",
|
|
48
64
|
# FFI
|
|
49
65
|
"GopherOrchLibrary",
|
|
66
|
+
# Auth
|
|
67
|
+
"GopherAuthError",
|
|
68
|
+
"ValidationResult",
|
|
69
|
+
"TokenPayload",
|
|
70
|
+
"GopherAuthContext",
|
|
71
|
+
"GopherAuthClient",
|
|
72
|
+
"GopherValidationOptions",
|
|
73
|
+
"gopher_init_auth_library",
|
|
74
|
+
"gopher_shutdown_auth_library",
|
|
75
|
+
"is_auth_available",
|
|
50
76
|
# Version
|
|
51
77
|
"__version__",
|
|
52
78
|
]
|
|
@@ -4,4 +4,11 @@ FFI bindings for the gopher-mcp-python native library.
|
|
|
4
4
|
|
|
5
5
|
from gopher_mcp_python.ffi.library import GopherOrchLibrary, GopherOrchHandle
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Auth module
|
|
8
|
+
from gopher_mcp_python.ffi import auth
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"GopherOrchLibrary",
|
|
12
|
+
"GopherOrchHandle",
|
|
13
|
+
"auth",
|
|
14
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auth FFI bindings for gopher-auth native library.
|
|
3
|
+
|
|
4
|
+
Provides Python bindings for JWT token validation and OAuth support
|
|
5
|
+
via the gopher-orch native library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from gopher_mcp_python.ffi.auth.types import (
|
|
9
|
+
GopherAuthError,
|
|
10
|
+
ERROR_DESCRIPTIONS,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
TokenPayload,
|
|
13
|
+
GopherAuthContext,
|
|
14
|
+
is_gopher_auth_error,
|
|
15
|
+
get_error_description,
|
|
16
|
+
gopher_create_empty_auth_context,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from gopher_mcp_python.ffi.auth.loader import (
|
|
20
|
+
GopherAuthClientPtr,
|
|
21
|
+
GopherAuthPayloadPtr,
|
|
22
|
+
GopherAuthOptionsPtr,
|
|
23
|
+
GopherAuthValidationResult,
|
|
24
|
+
load_library,
|
|
25
|
+
is_library_loaded,
|
|
26
|
+
get_library,
|
|
27
|
+
is_auth_available,
|
|
28
|
+
get_auth_functions,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from gopher_mcp_python.ffi.auth.validation_options import (
|
|
32
|
+
GopherValidationOptions,
|
|
33
|
+
gopher_create_validation_options,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from gopher_mcp_python.ffi.auth.auth_client import (
|
|
37
|
+
GopherAuthClient,
|
|
38
|
+
gopher_init_auth_library,
|
|
39
|
+
gopher_shutdown_auth_library,
|
|
40
|
+
gopher_get_auth_library_version,
|
|
41
|
+
gopher_is_auth_library_initialized,
|
|
42
|
+
gopher_generate_www_authenticate_header,
|
|
43
|
+
gopher_generate_www_authenticate_header_v2,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Enums
|
|
48
|
+
"GopherAuthError",
|
|
49
|
+
# Constants
|
|
50
|
+
"ERROR_DESCRIPTIONS",
|
|
51
|
+
# Dataclasses
|
|
52
|
+
"ValidationResult",
|
|
53
|
+
"TokenPayload",
|
|
54
|
+
"GopherAuthContext",
|
|
55
|
+
# Type functions
|
|
56
|
+
"is_gopher_auth_error",
|
|
57
|
+
"get_error_description",
|
|
58
|
+
"gopher_create_empty_auth_context",
|
|
59
|
+
# Pointer types
|
|
60
|
+
"GopherAuthClientPtr",
|
|
61
|
+
"GopherAuthPayloadPtr",
|
|
62
|
+
"GopherAuthOptionsPtr",
|
|
63
|
+
# Structures
|
|
64
|
+
"GopherAuthValidationResult",
|
|
65
|
+
# Loader functions
|
|
66
|
+
"load_library",
|
|
67
|
+
"is_library_loaded",
|
|
68
|
+
"get_library",
|
|
69
|
+
"is_auth_available",
|
|
70
|
+
"get_auth_functions",
|
|
71
|
+
# Validation options
|
|
72
|
+
"GopherValidationOptions",
|
|
73
|
+
"gopher_create_validation_options",
|
|
74
|
+
# Auth client
|
|
75
|
+
"GopherAuthClient",
|
|
76
|
+
"gopher_init_auth_library",
|
|
77
|
+
"gopher_shutdown_auth_library",
|
|
78
|
+
"gopher_get_auth_library_version",
|
|
79
|
+
"gopher_is_auth_library_initialized",
|
|
80
|
+
"gopher_generate_www_authenticate_header",
|
|
81
|
+
"gopher_generate_www_authenticate_header_v2",
|
|
82
|
+
]
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AuthClient - JWT token validation client.
|
|
3
|
+
|
|
4
|
+
Provides high-level API for validating JWT tokens and extracting
|
|
5
|
+
payload information using the gopher-auth native library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ctypes import byref, c_char_p, c_int64, c_void_p
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from gopher_mcp_python.ffi.auth.loader import (
|
|
12
|
+
load_library,
|
|
13
|
+
get_auth_functions,
|
|
14
|
+
is_auth_available,
|
|
15
|
+
GopherAuthValidationResult,
|
|
16
|
+
)
|
|
17
|
+
from gopher_mcp_python.ffi.auth.types import (
|
|
18
|
+
GopherAuthError,
|
|
19
|
+
ValidationResult,
|
|
20
|
+
TokenPayload,
|
|
21
|
+
get_error_description,
|
|
22
|
+
)
|
|
23
|
+
from gopher_mcp_python.ffi.auth.validation_options import GopherValidationOptions
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Library Lifecycle Functions
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
_auth_initialized: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def gopher_init_auth_library() -> bool:
|
|
34
|
+
"""
|
|
35
|
+
Initialize the auth library.
|
|
36
|
+
|
|
37
|
+
This must be called before using any auth functions.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if initialization successful, False otherwise.
|
|
41
|
+
"""
|
|
42
|
+
global _auth_initialized
|
|
43
|
+
|
|
44
|
+
if _auth_initialized:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
if not load_library():
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
if not is_auth_available():
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
funcs = get_auth_functions()
|
|
54
|
+
auth_init = funcs.get("auth_init")
|
|
55
|
+
if auth_init is None:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
result = auth_init()
|
|
59
|
+
if result == GopherAuthError.SUCCESS:
|
|
60
|
+
_auth_initialized = True
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def gopher_shutdown_auth_library() -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Shutdown the auth library and release resources.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if shutdown successful, False otherwise.
|
|
72
|
+
"""
|
|
73
|
+
global _auth_initialized
|
|
74
|
+
|
|
75
|
+
if not _auth_initialized:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
funcs = get_auth_functions()
|
|
79
|
+
auth_shutdown = funcs.get("auth_shutdown")
|
|
80
|
+
if auth_shutdown is None:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
result = auth_shutdown()
|
|
84
|
+
if result == GopherAuthError.SUCCESS:
|
|
85
|
+
_auth_initialized = False
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def gopher_get_auth_library_version() -> Optional[str]:
|
|
92
|
+
"""
|
|
93
|
+
Get the auth library version string.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Version string, or None if unavailable.
|
|
97
|
+
"""
|
|
98
|
+
if not load_library():
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
funcs = get_auth_functions()
|
|
102
|
+
auth_version = funcs.get("auth_version")
|
|
103
|
+
if auth_version is None:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
result = auth_version()
|
|
107
|
+
if result:
|
|
108
|
+
return result.decode("utf-8")
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def gopher_is_auth_library_initialized() -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Check if the auth library has been initialized.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
True if initialized, False otherwise.
|
|
118
|
+
"""
|
|
119
|
+
return _auth_initialized
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ============================================================================
|
|
123
|
+
# WWW-Authenticate Header Generation
|
|
124
|
+
# ============================================================================
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def gopher_generate_www_authenticate_header(
|
|
128
|
+
realm: str,
|
|
129
|
+
error: str = "",
|
|
130
|
+
description: str = "",
|
|
131
|
+
) -> Optional[str]:
|
|
132
|
+
"""
|
|
133
|
+
Generate a WWW-Authenticate header for 401 responses.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
realm: The authentication realm.
|
|
137
|
+
error: Optional error code (e.g., "invalid_token").
|
|
138
|
+
description: Optional error description.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The WWW-Authenticate header value, or None on error.
|
|
142
|
+
"""
|
|
143
|
+
if not load_library():
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
funcs = get_auth_functions()
|
|
147
|
+
generate_func = funcs.get("generate_www_authenticate")
|
|
148
|
+
if generate_func is None:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
header_out = c_char_p()
|
|
152
|
+
result = generate_func(
|
|
153
|
+
realm.encode("utf-8"),
|
|
154
|
+
error.encode("utf-8"),
|
|
155
|
+
description.encode("utf-8"),
|
|
156
|
+
byref(header_out),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if result != GopherAuthError.SUCCESS:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if header_out.value:
|
|
163
|
+
header = header_out.value.decode("utf-8")
|
|
164
|
+
# Free the allocated string
|
|
165
|
+
free_string = funcs.get("free_string")
|
|
166
|
+
if free_string:
|
|
167
|
+
free_string(header_out)
|
|
168
|
+
return header
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def gopher_generate_www_authenticate_header_v2(
|
|
174
|
+
realm: str,
|
|
175
|
+
resource_metadata_url: str,
|
|
176
|
+
scopes: List[str],
|
|
177
|
+
error: str = "",
|
|
178
|
+
error_description: str = "",
|
|
179
|
+
) -> Optional[str]:
|
|
180
|
+
"""
|
|
181
|
+
Generate a WWW-Authenticate header with RFC 9728 support.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
realm: The authentication realm.
|
|
185
|
+
resource_metadata_url: URL to the OAuth protected resource metadata.
|
|
186
|
+
scopes: List of required scopes.
|
|
187
|
+
error: Optional error code.
|
|
188
|
+
error_description: Optional error description.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
The WWW-Authenticate header value, or None on error.
|
|
192
|
+
"""
|
|
193
|
+
if not load_library():
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
funcs = get_auth_functions()
|
|
197
|
+
generate_func = funcs.get("generate_www_authenticate_v2")
|
|
198
|
+
if generate_func is None:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
scopes_str = " ".join(scopes)
|
|
202
|
+
header_out = c_char_p()
|
|
203
|
+
result = generate_func(
|
|
204
|
+
realm.encode("utf-8"),
|
|
205
|
+
resource_metadata_url.encode("utf-8"),
|
|
206
|
+
scopes_str.encode("utf-8"),
|
|
207
|
+
error.encode("utf-8"),
|
|
208
|
+
error_description.encode("utf-8"),
|
|
209
|
+
byref(header_out),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if result != GopherAuthError.SUCCESS:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
if header_out.value:
|
|
216
|
+
header = header_out.value.decode("utf-8")
|
|
217
|
+
# Free the allocated string
|
|
218
|
+
free_string = funcs.get("free_string")
|
|
219
|
+
if free_string:
|
|
220
|
+
free_string(header_out)
|
|
221
|
+
return header
|
|
222
|
+
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ============================================================================
|
|
227
|
+
# AuthClient Class
|
|
228
|
+
# ============================================================================
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class GopherAuthClient:
|
|
232
|
+
"""
|
|
233
|
+
JWT token validation client.
|
|
234
|
+
|
|
235
|
+
Provides methods for validating JWT tokens and extracting payload
|
|
236
|
+
information using JWKS-based signature verification.
|
|
237
|
+
|
|
238
|
+
Usage:
|
|
239
|
+
# Using context manager (recommended)
|
|
240
|
+
with GopherAuthClient("https://auth.example.com/.well-known/jwks.json",
|
|
241
|
+
"https://auth.example.com") as client:
|
|
242
|
+
result = client.validate_token(token)
|
|
243
|
+
if result.valid:
|
|
244
|
+
payload = client.extract_payload(token)
|
|
245
|
+
|
|
246
|
+
# Manual lifecycle management
|
|
247
|
+
client = GopherAuthClient(jwks_uri, issuer)
|
|
248
|
+
try:
|
|
249
|
+
result, payload = client.validate_and_extract(token)
|
|
250
|
+
finally:
|
|
251
|
+
client.destroy()
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def __init__(self, jwks_uri: str, issuer: str) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Create a new AuthClient instance.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
jwks_uri: URL to the JWKS endpoint.
|
|
260
|
+
issuer: Expected token issuer.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
RuntimeError: If the library is not loaded or client creation fails.
|
|
264
|
+
"""
|
|
265
|
+
self._handle: Optional[c_void_p] = None
|
|
266
|
+
self._destroyed: bool = False
|
|
267
|
+
|
|
268
|
+
if not load_library():
|
|
269
|
+
raise RuntimeError("Failed to load gopher-orch library")
|
|
270
|
+
|
|
271
|
+
if not is_auth_available():
|
|
272
|
+
raise RuntimeError("Auth functions not available in library")
|
|
273
|
+
|
|
274
|
+
# Initialize library if needed
|
|
275
|
+
if not gopher_is_auth_library_initialized():
|
|
276
|
+
if not gopher_init_auth_library():
|
|
277
|
+
raise RuntimeError("Failed to initialize auth library")
|
|
278
|
+
|
|
279
|
+
funcs = get_auth_functions()
|
|
280
|
+
client_create = funcs.get("client_create")
|
|
281
|
+
if client_create is None:
|
|
282
|
+
raise RuntimeError("client_create function not available")
|
|
283
|
+
|
|
284
|
+
handle = c_void_p()
|
|
285
|
+
result = client_create(
|
|
286
|
+
byref(handle),
|
|
287
|
+
jwks_uri.encode("utf-8"),
|
|
288
|
+
issuer.encode("utf-8"),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if result != GopherAuthError.SUCCESS:
|
|
292
|
+
raise RuntimeError(
|
|
293
|
+
f"Failed to create auth client: {get_error_description(result)}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
self._handle = handle
|
|
297
|
+
|
|
298
|
+
def set_option(self, key: str, value: str) -> bool:
|
|
299
|
+
"""
|
|
300
|
+
Set a client option.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
key: Option name (e.g., "cache_duration", "auto_refresh", "request_timeout").
|
|
304
|
+
value: Option value as string.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
True if option was set successfully, False otherwise.
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
RuntimeError: If the client has been destroyed.
|
|
311
|
+
"""
|
|
312
|
+
self._ensure_not_destroyed()
|
|
313
|
+
|
|
314
|
+
funcs = get_auth_functions()
|
|
315
|
+
client_set_option = funcs.get("client_set_option")
|
|
316
|
+
if client_set_option is None:
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
result = client_set_option(
|
|
320
|
+
self._handle,
|
|
321
|
+
key.encode("utf-8"),
|
|
322
|
+
value.encode("utf-8"),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
return result == GopherAuthError.SUCCESS
|
|
326
|
+
|
|
327
|
+
def validate_token(
|
|
328
|
+
self,
|
|
329
|
+
token: str,
|
|
330
|
+
options: Optional[GopherValidationOptions] = None,
|
|
331
|
+
) -> ValidationResult:
|
|
332
|
+
"""
|
|
333
|
+
Validate a JWT token.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
token: The JWT token string to validate.
|
|
337
|
+
options: Optional validation options (scopes, audience, etc.).
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
ValidationResult with valid flag, error code, and message.
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
RuntimeError: If the client has been destroyed or function unavailable.
|
|
344
|
+
"""
|
|
345
|
+
self._ensure_not_destroyed()
|
|
346
|
+
|
|
347
|
+
funcs = get_auth_functions()
|
|
348
|
+
validate_token = funcs.get("validate_token")
|
|
349
|
+
if validate_token is None:
|
|
350
|
+
return ValidationResult(
|
|
351
|
+
valid=False,
|
|
352
|
+
error_code=GopherAuthError.NOT_INITIALIZED,
|
|
353
|
+
error_message="validate_token function not available",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
result_struct = GopherAuthValidationResult()
|
|
357
|
+
options_handle = options.get_handle() if options else None
|
|
358
|
+
|
|
359
|
+
err = validate_token(
|
|
360
|
+
self._handle,
|
|
361
|
+
token.encode("utf-8"),
|
|
362
|
+
options_handle,
|
|
363
|
+
byref(result_struct),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if err != GopherAuthError.SUCCESS:
|
|
367
|
+
return ValidationResult(
|
|
368
|
+
valid=False,
|
|
369
|
+
error_code=err,
|
|
370
|
+
error_message=get_error_description(err),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
error_message = None
|
|
374
|
+
if result_struct.error_message:
|
|
375
|
+
error_message = result_struct.error_message.decode("utf-8")
|
|
376
|
+
|
|
377
|
+
return ValidationResult(
|
|
378
|
+
valid=result_struct.valid,
|
|
379
|
+
error_code=result_struct.error_code,
|
|
380
|
+
error_message=error_message,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def extract_payload(self, token: str) -> Optional[TokenPayload]:
|
|
384
|
+
"""
|
|
385
|
+
Extract payload from a JWT token without validation.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
token: The JWT token string.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
TokenPayload with extracted claims, or None on error.
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
RuntimeError: If the client has been destroyed.
|
|
395
|
+
"""
|
|
396
|
+
self._ensure_not_destroyed()
|
|
397
|
+
|
|
398
|
+
funcs = get_auth_functions()
|
|
399
|
+
extract_payload = funcs.get("extract_payload")
|
|
400
|
+
if extract_payload is None:
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
payload_handle = c_void_p()
|
|
404
|
+
result = extract_payload(
|
|
405
|
+
token.encode("utf-8"),
|
|
406
|
+
byref(payload_handle),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if result != GopherAuthError.SUCCESS:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
subject = self._get_payload_string(payload_handle, "payload_get_subject")
|
|
414
|
+
scopes = self._get_payload_string(payload_handle, "payload_get_scopes")
|
|
415
|
+
audience = self._get_payload_string(payload_handle, "payload_get_audience")
|
|
416
|
+
issuer = self._get_payload_string(payload_handle, "payload_get_issuer")
|
|
417
|
+
|
|
418
|
+
# Get expiration
|
|
419
|
+
expiration = None
|
|
420
|
+
get_exp = funcs.get("payload_get_expiration")
|
|
421
|
+
if get_exp:
|
|
422
|
+
exp_value = c_int64()
|
|
423
|
+
if get_exp(payload_handle, byref(exp_value)) == GopherAuthError.SUCCESS:
|
|
424
|
+
expiration = exp_value.value
|
|
425
|
+
|
|
426
|
+
return TokenPayload(
|
|
427
|
+
subject=subject or "",
|
|
428
|
+
scopes=scopes or "",
|
|
429
|
+
audience=audience,
|
|
430
|
+
expiration=expiration,
|
|
431
|
+
issuer=issuer,
|
|
432
|
+
)
|
|
433
|
+
finally:
|
|
434
|
+
# Clean up payload handle
|
|
435
|
+
payload_destroy = funcs.get("payload_destroy")
|
|
436
|
+
if payload_destroy:
|
|
437
|
+
payload_destroy(payload_handle)
|
|
438
|
+
|
|
439
|
+
def _get_payload_string(
|
|
440
|
+
self,
|
|
441
|
+
payload_handle: c_void_p,
|
|
442
|
+
func_name: str,
|
|
443
|
+
) -> Optional[str]:
|
|
444
|
+
"""
|
|
445
|
+
Helper to get a string field from payload.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
payload_handle: The payload handle.
|
|
449
|
+
func_name: Name of the getter function.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
The string value, or None if unavailable.
|
|
453
|
+
"""
|
|
454
|
+
funcs = get_auth_functions()
|
|
455
|
+
getter = funcs.get(func_name)
|
|
456
|
+
if getter is None:
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
value_out = c_char_p()
|
|
460
|
+
result = getter(payload_handle, byref(value_out))
|
|
461
|
+
|
|
462
|
+
if result != GopherAuthError.SUCCESS:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
if value_out.value:
|
|
466
|
+
value = value_out.value.decode("utf-8")
|
|
467
|
+
# Free the allocated string
|
|
468
|
+
free_string = funcs.get("free_string")
|
|
469
|
+
if free_string:
|
|
470
|
+
free_string(value_out)
|
|
471
|
+
return value
|
|
472
|
+
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
def validate_and_extract(
|
|
476
|
+
self,
|
|
477
|
+
token: str,
|
|
478
|
+
options: Optional[GopherValidationOptions] = None,
|
|
479
|
+
) -> Tuple[ValidationResult, Optional[TokenPayload]]:
|
|
480
|
+
"""
|
|
481
|
+
Validate a token and extract its payload in one call.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
token: The JWT token string.
|
|
485
|
+
options: Optional validation options.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
Tuple of (ValidationResult, TokenPayload or None).
|
|
489
|
+
|
|
490
|
+
Raises:
|
|
491
|
+
RuntimeError: If the client has been destroyed.
|
|
492
|
+
"""
|
|
493
|
+
result = self.validate_token(token, options)
|
|
494
|
+
|
|
495
|
+
if not result.valid:
|
|
496
|
+
return result, None
|
|
497
|
+
|
|
498
|
+
payload = self.extract_payload(token)
|
|
499
|
+
return result, payload
|
|
500
|
+
|
|
501
|
+
def destroy(self) -> None:
|
|
502
|
+
"""
|
|
503
|
+
Destroy the client and release resources.
|
|
504
|
+
|
|
505
|
+
This method is idempotent - calling it multiple times is safe.
|
|
506
|
+
"""
|
|
507
|
+
if self._destroyed or self._handle is None:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
funcs = get_auth_functions()
|
|
511
|
+
client_destroy = funcs.get("client_destroy")
|
|
512
|
+
if client_destroy is not None:
|
|
513
|
+
client_destroy(self._handle)
|
|
514
|
+
|
|
515
|
+
self._handle = None
|
|
516
|
+
self._destroyed = True
|
|
517
|
+
|
|
518
|
+
def is_destroyed(self) -> bool:
|
|
519
|
+
"""
|
|
520
|
+
Check if this client has been destroyed.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
True if destroyed, False otherwise.
|
|
524
|
+
"""
|
|
525
|
+
return self._destroyed
|
|
526
|
+
|
|
527
|
+
def _ensure_not_destroyed(self) -> None:
|
|
528
|
+
"""
|
|
529
|
+
Ensure the client has not been destroyed.
|
|
530
|
+
|
|
531
|
+
Raises:
|
|
532
|
+
RuntimeError: If the client has been destroyed.
|
|
533
|
+
"""
|
|
534
|
+
if self._destroyed:
|
|
535
|
+
raise RuntimeError("GopherAuthClient has been destroyed")
|
|
536
|
+
|
|
537
|
+
def __enter__(self) -> "GopherAuthClient":
|
|
538
|
+
"""Enter context manager."""
|
|
539
|
+
return self
|
|
540
|
+
|
|
541
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
542
|
+
"""Exit context manager and destroy resources."""
|
|
543
|
+
self.destroy()
|
|
544
|
+
|
|
545
|
+
def __del__(self) -> None:
|
|
546
|
+
"""Destructor - ensure resources are released."""
|
|
547
|
+
self.destroy()
|