6
6
from base64 import b64encode , b64decode
7
7
8
8
import gssapi
9
+ from gssapi import SecurityContext as SecCtx
9
10
from gssapi .exceptions import GSSError
10
11
11
12
import httpx
34
35
_find_auth = re .compile (r'Negotiate\s*([^,]*)' , re .I ).search
35
36
36
37
37
- def _negotiate_value (response : Response ) -> Optional [str ]:
38
+ def _negotiate_value (response : Response ) -> Optional [bytes ]:
38
39
"""Extracts the gssapi authentication token from the appropriate header"""
39
40
authreq = response .headers .get ('www-authenticate' , None )
40
41
if authreq :
@@ -71,23 +72,27 @@ def _handle_gsserror(*, gss_stage: str, result: Any):
71
72
The result to return if a GSSError is raised. If it's an Exception
72
73
type, then it will be raised with the logged message.
73
74
"""
75
+
74
76
def _decor (func ):
75
77
@wraps (func )
76
78
def _wrapper (* args , ** kwargs ):
77
79
try :
78
80
return func (* args , ** kwargs )
79
- except gssapi . exceptions . GSSError as error :
81
+ except GSSError as error :
80
82
msg = f"{ gss_stage } context failed: { error .gen_message ()} "
81
83
log .exception (f"{ func .__name__ } (): { msg } " )
82
84
if isinstance (result , type ) and issubclass (result , Exception ):
83
85
raise result (msg )
84
86
return result
87
+
85
88
return _wrapper
89
+
86
90
return _decor
87
91
88
92
89
93
class HTTPSPNEGOAuth (Auth ):
90
- """Attaches HTTP GSSAPI Authentication to the given Request object.
94
+ """
95
+ Attaches HTTP GSSAPI Authentication to the given Request object.
91
96
92
97
`mutual_authentication` controls whether GSSAPI should attempt mutual
93
98
authentication. It may be `REQUIRED`, `OPTIONAL`, or `DISABLED`
@@ -112,6 +117,7 @@ class HTTPSPNEGOAuth(Auth):
112
117
server responses. See the `SanitizedResponse` class.
113
118
114
119
"""
120
+
115
121
def __init__ (self ,
116
122
mutual_authentication : int = DISABLED ,
117
123
target_name : Optional [str ] = "HTTP" ,
@@ -120,7 +126,6 @@ def __init__(self,
120
126
creds : gssapi .Credentials = None ,
121
127
mech : bytes = None ,
122
128
sanitize_mutual_error_response : bool = True ):
123
- self .context = {}
124
129
self .mutual_authentication = mutual_authentication
125
130
self .target_name = target_name
126
131
self .delegate = delegate
@@ -132,42 +137,41 @@ def __init__(self,
132
137
def auth_flow (self , request : Request ) -> FlowGen :
133
138
if self .opportunistic_auth :
134
139
# add Authorization header before we receive a 401
135
- auth_header = self .generate_request_header (request .url .host )
136
-
137
- log .debug (f"Preemptive Authorization header: { auth_header } " )
138
- request .headers ['Authorization' ] = auth_header
140
+ ctx = self .set_auth_header (request )
141
+ else :
142
+ ctx = None
139
143
140
144
response = yield request
141
- yield from self .handle_response (response )
145
+ yield from self .handle_response (response , ctx )
142
146
143
- def handle_response (self , response : Response ) -> FlowGen :
147
+ def handle_response (self ,
148
+ response : Response ,
149
+ ctx : SecCtx = None ) -> FlowGen :
144
150
num_401s = 0
145
151
while response .status_code == 401 and num_401s < 2 :
146
152
num_401s += 1
147
153
log .debug (f"Handling 401 response, total seen: { num_401s } " )
148
- try :
149
- response = yield self . handle_401 (response )
150
- except httpx . ProtocolError : # GSSAPI isn't supported
154
+
155
+ if _negotiate_value (response ) is None :
156
+ log . debug ( " GSSAPI is not supported" )
151
157
break
152
158
153
- if response .status_code == 401 :
154
- log .debug (f"Failed to authenticate, returning 401 response" )
155
- return
159
+ log .debug ("Generating user authentication header" )
160
+ try :
161
+ ctx = self .set_auth_header (response .request , response )
162
+ except SPNEGOExchangeError :
163
+ log .debug ("Failed to generate authentication header" )
156
164
157
- self .handle_mutual_auth (response )
165
+ # Try request again, hopefully with a new auth header
166
+ response = yield response .request
158
167
159
- def handle_401 (self , response : Response ) -> Request :
160
- """Handles 401's, attempts to use GSSAPI authentication"""
161
- log .debug ("handle_401(): Handling 401" )
162
- if _negotiate_value (response ) is None :
163
- log .debug ("handle_401(): GSSAPI is not supported" )
164
- raise httpx .ProtocolError ("GSSAPI is not supported" )
168
+ if response .status_code == 401 or ctx is None :
169
+ log .debug ("Failed to authenticate, returning 401 response" )
170
+ return
165
171
166
- request = self .authenticate_user (response )
167
- log .debug (f"handle_401(): returning { request } " )
168
- return request
172
+ self .handle_mutual_auth (response , ctx )
169
173
170
- def handle_mutual_auth (self , response : Response ):
174
+ def handle_mutual_auth (self , response : Response , ctx : SecCtx ):
171
175
"""
172
176
Handles all responses with the exception of 401s.
173
177
@@ -176,14 +180,14 @@ def handle_mutual_auth(self, response: Response):
176
180
log .debug (f"handle_mutual_auth(): Handling { response .status_code } " )
177
181
178
182
if self .mutual_authentication == DISABLED :
179
- log .debug (f "handle_mutual_auth(): Mutual auth disabled, ignoring" )
183
+ log .debug ("handle_mutual_auth(): Mutual auth disabled, ignoring" )
180
184
return
181
185
182
186
is_http_error = response .status_code >= 400
183
187
184
188
if _negotiate_value (response ) is not None :
185
189
log .debug ("handle_mutual_auth(): Authenticating the server" )
186
- if not self .authenticate_server (response ):
190
+ if not self .authenticate_server (response , ctx ):
187
191
# Mutual authentication failure when mutual auth is wanted,
188
192
# raise an exception so the user doesn't use an untrusted
189
193
# response.
@@ -209,52 +213,49 @@ def handle_mutual_auth(self, response: Response):
209
213
raise MutualAuthenticationError (response = response )
210
214
211
215
@_handle_gsserror (gss_stage = 'stepping' , result = SPNEGOExchangeError )
212
- def generate_request_header (self ,
213
- host : str ,
214
- response : Response = None ) -> str :
216
+ def set_auth_header (self ,
217
+ request : Request ,
218
+ response : Response = None ) -> SecCtx :
215
219
"""
216
- Generates the GSSAPI authentication token
220
+ Create a new security context, generate the GSSAPI authentication
221
+ token, and insert it into the request header. The new security context
222
+ will be returned.
217
223
218
- If any GSSAPI step fails, raise SPNEGOExchangeError
219
- with failure detail.
224
+ If any GSSAPI step fails, raise SPNEGOExchangeError with failure detail.
220
225
"""
221
- self .context [host ] = self ._make_context (host )
222
-
223
- token = _negotiate_value (response ) if response else None
224
- gss_resp = self .context [host ].step (token )
225
- return f"Negotiate { b64encode (gss_resp ).decode ()} "
226
-
227
- def authenticate_user (self , response : Response ) -> Request :
228
- """Handles user authentication with GSSAPI"""
229
- host = response .url .host
230
- try :
231
- auth_header = self .generate_request_header (host , response )
232
- except SPNEGOExchangeError : # GSS Failure, return existing response
233
- log .debug (f"authenticate_user(): Failed to generate auth header" )
234
- else :
235
- log .debug (f"authenticate_user(): Auth header: { auth_header } " )
236
- response .request .headers ['Authorization' ] = auth_header
226
+ ctx = self ._make_context (request .url .host )
227
+
228
+ token = _negotiate_value (response ) if response is not None else None
229
+ gss_resp = ctx .step (token or None )
230
+ auth_header = f"Negotiate { b64encode (gss_resp ).decode ()} "
231
+ log .debug (
232
+ f"add_request_header(): "
233
+ f"{ 'Preemptive ' if token is None else '' } "
234
+ f"Authorization header: { auth_header } "
235
+ )
237
236
238
- return response .request
237
+ request .headers ['Authorization' ] = auth_header
238
+ return ctx
239
239
240
240
@_handle_gsserror (gss_stage = "stepping" , result = False )
241
- def authenticate_server (self , response : Response ) -> bool :
241
+ def authenticate_server (self , response : Response , ctx : SecCtx ) -> bool :
242
242
"""
243
- Uses GSSAPI to authenticate the server.
243
+ Uses GSSAPI to authenticate the server by extracting the negotiate
244
+ value from the response and stepping the security context.
244
245
245
246
Returns True on success, False on failure.
246
247
"""
247
248
auth_header = _negotiate_value (response )
248
249
log .debug (f"authenticate_server(): Authenticate header: { auth_header } " )
249
250
250
251
# If the handshake isn't complete here, nothing we can do
251
- self . context [ response . url . host ] .step (auth_header )
252
+ ctx .step (auth_header )
252
253
253
254
log .debug ("authenticate_server(): authentication successful" )
254
255
return True
255
256
256
257
@_handle_gsserror (gss_stage = "initializing" , result = SPNEGOExchangeError )
257
- def _make_context (self , host : str ) -> gssapi . SecurityContext :
258
+ def _make_context (self , host : str ) -> SecCtx :
258
259
"""
259
260
Create a GSSAPI security context for handling the authentication.
260
261
@@ -268,7 +269,7 @@ def _make_context(self, host: str) -> gssapi.SecurityContext:
268
269
name += f"@{ host } "
269
270
name = gssapi .Name (name , gssapi .NameType .hostbased_service )
270
271
271
- return gssapi . SecurityContext (
272
+ return SecCtx (
272
273
usage = "initiate" ,
273
274
flags = self ._gssflags ,
274
275
name = name ,
0 commit comments