Skip to content

Commit cc9df64

Browse files
committed
Merge branch 'master' into secret-support
2 parents ffa89ec + 3f83f5d commit cc9df64

File tree

6 files changed

+113
-162
lines changed

6 files changed

+113
-162
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ venv/*
3030
env/*
3131

3232
.vscode/*
33+
.claude/
3334

3435
# ignore http server log
3536
atests/http_server/http_server.log

atests/http_server/core.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
# See AUTHORS and LICENSE for more information
33

44
from flask import Flask, Response, jsonify as flask_jsonify, request
5+
from flask_httpauth import HTTPBasicAuth, HTTPDigestAuth
56

67
from .structures import CaseInsensitiveDict
78
from .helpers import get_dict, status_code
89
from .utils import weighted_choice
910

1011

1112
app = Flask(__name__)
13+
app.config['SECRET_KEY'] = 'test-secret-key-for-digest-auth'
14+
15+
# Initialize authentication handlers
16+
basic_auth = HTTPBasicAuth()
17+
digest_auth = HTTPDigestAuth()
1218

1319

1420
def jsonify(*args, **kwargs):
@@ -191,3 +197,92 @@ def redirect_to():
191197
response.headers["Location"] = args["url"]
192198

193199
return response
200+
201+
202+
# Basic auth verification callback
203+
@basic_auth.verify_password
204+
def verify_basic_password(username, password):
205+
# Get expected credentials from the request path
206+
path_parts = request.path.split('/')
207+
if len(path_parts) >= 4 and path_parts[1] == 'basic-auth':
208+
expected_user = path_parts[2]
209+
expected_pass = path_parts[3]
210+
return username == expected_user and password == expected_pass
211+
return False
212+
213+
214+
@app.route("/basic-auth/<user>/<passwd>")
215+
@basic_auth.login_required
216+
def basic_auth_endpoint(user, passwd):
217+
"""Prompts the user for authorization using HTTP Basic Auth.
218+
---
219+
tags:
220+
- Auth
221+
parameters:
222+
- in: path
223+
name: user
224+
type: string
225+
required: true
226+
- in: path
227+
name: passwd
228+
type: string
229+
required: true
230+
produces:
231+
- application/json
232+
responses:
233+
200:
234+
description: Successful authentication.
235+
401:
236+
description: Unsuccessful authentication.
237+
"""
238+
return jsonify(authenticated=True, user=basic_auth.current_user())
239+
240+
241+
# Digest auth password callback
242+
@digest_auth.get_password
243+
def get_digest_password(username):
244+
# Get expected credentials from the request path
245+
path_parts = request.path.split('/')
246+
if len(path_parts) >= 5 and path_parts[1] == 'digest-auth':
247+
expected_user = path_parts[3]
248+
expected_pass = path_parts[4]
249+
if username == expected_user:
250+
return expected_pass
251+
return None
252+
253+
254+
@app.route("/digest-auth/<qop>/<user>/<passwd>")
255+
@app.route("/digest-auth/<qop>/<user>/<passwd>/<algorithm>")
256+
@digest_auth.login_required
257+
def digest_auth_endpoint(qop, user, passwd, algorithm='MD5'):
258+
"""Prompts the user for authorization using HTTP Digest Auth.
259+
---
260+
tags:
261+
- Auth
262+
parameters:
263+
- in: path
264+
name: qop
265+
type: string
266+
required: true
267+
- in: path
268+
name: user
269+
type: string
270+
required: true
271+
- in: path
272+
name: passwd
273+
type: string
274+
required: true
275+
- in: path
276+
name: algorithm
277+
type: string
278+
required: false
279+
default: MD5
280+
produces:
281+
- application/json
282+
responses:
283+
200:
284+
description: Successful authentication.
285+
401:
286+
description: Unsuccessful authentication.
287+
"""
288+
return jsonify(authenticated=True, user=digest_auth.current_user())

atests/http_server/helpers.py

Lines changed: 0 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -256,126 +256,6 @@ def status_code(code):
256256
return r
257257

258258

259-
def check_basic_auth(user, passwd):
260-
"""Checks user authentication using HTTP Basic Auth."""
261-
262-
auth = request.authorization
263-
return auth and auth.username == user and auth.password == passwd
264-
265-
266-
# Digest auth helpers
267-
# qop is a quality of protection
268-
269-
270-
def H(data, algorithm):
271-
if algorithm == 'SHA-256':
272-
return sha256(data).hexdigest()
273-
elif algorithm == 'SHA-512':
274-
return sha512(data).hexdigest()
275-
else:
276-
return md5(data).hexdigest()
277-
278-
279-
def HA1(realm, username, password, algorithm):
280-
"""Create HA1 hash by realm, username, password
281-
282-
HA1 = md5(A1) = MD5(username:realm:password)
283-
"""
284-
if not realm:
285-
realm = u''
286-
return H(b":".join([username.encode('utf-8'),
287-
realm.encode('utf-8'),
288-
password.encode('utf-8')]), algorithm)
289-
290-
291-
def HA2(credentials, request, algorithm):
292-
"""Create HA2 md5 hash
293-
294-
If the qop directive's value is "auth" or is unspecified, then HA2:
295-
HA2 = md5(A2) = MD5(method:digestURI)
296-
If the qop directive's value is "auth-int" , then HA2 is
297-
HA2 = md5(A2) = MD5(method:digestURI:MD5(entityBody))
298-
"""
299-
if credentials.get("qop") == "auth" or credentials.get('qop') is None:
300-
return H(b":".join([request['method'].encode('utf-8'), request['uri'].encode('utf-8')]), algorithm)
301-
elif credentials.get("qop") == "auth-int":
302-
for k in 'method', 'uri', 'body':
303-
if k not in request:
304-
raise ValueError("%s required" % k)
305-
A2 = b":".join([request['method'].encode('utf-8'),
306-
request['uri'].encode('utf-8'),
307-
H(request['body'], algorithm).encode('utf-8')])
308-
return H(A2, algorithm)
309-
raise ValueError
310-
311-
312-
def response(credentials, password, request):
313-
"""Compile digest auth response
314-
315-
If the qop directive's value is "auth" or "auth-int" , then compute the response as follows:
316-
RESPONSE = MD5(HA1:nonce:nonceCount:clienNonce:qop:HA2)
317-
Else if the qop directive is unspecified, then compute the response as follows:
318-
RESPONSE = MD5(HA1:nonce:HA2)
319-
320-
Arguments:
321-
- `credentials`: credentials dict
322-
- `password`: request user password
323-
- `request`: request dict
324-
"""
325-
response = None
326-
algorithm = credentials.get('algorithm')
327-
HA1_value = HA1(
328-
credentials.get('realm'),
329-
credentials.get('username'),
330-
password,
331-
algorithm
332-
)
333-
HA2_value = HA2(credentials, request, algorithm)
334-
if credentials.get('qop') is None:
335-
response = H(b":".join([
336-
HA1_value.encode('utf-8'),
337-
credentials.get('nonce', '').encode('utf-8'),
338-
HA2_value.encode('utf-8')
339-
]), algorithm)
340-
elif credentials.get('qop') == 'auth' or credentials.get('qop') == 'auth-int':
341-
for k in 'nonce', 'nc', 'cnonce', 'qop':
342-
if k not in credentials:
343-
raise ValueError("%s required for response H" % k)
344-
response = H(b":".join([HA1_value.encode('utf-8'),
345-
credentials.get('nonce').encode('utf-8'),
346-
credentials.get('nc').encode('utf-8'),
347-
credentials.get('cnonce').encode('utf-8'),
348-
credentials.get('qop').encode('utf-8'),
349-
HA2_value.encode('utf-8')]), algorithm)
350-
else:
351-
raise ValueError("qop value are wrong")
352-
353-
return response
354-
355-
356-
def check_digest_auth(user, passwd):
357-
"""Check user authentication using HTTP Digest auth"""
358-
359-
if request.headers.get('Authorization'):
360-
credentials = Authorization.from_header(request.headers.get('Authorization'))
361-
if not credentials:
362-
return
363-
request_uri = request.script_root + request.path
364-
if request.query_string:
365-
request_uri += '?' + request.query_string
366-
response_hash = response(credentials, passwd, dict(uri=request_uri,
367-
body=request.data,
368-
method=request.method))
369-
if credentials.get('response') == response_hash:
370-
return True
371-
return False
372-
373-
374-
def secure_cookie():
375-
"""Return true if cookie should have secure attribute"""
376-
return request.environ['wsgi.url_scheme'] == 'https'
377-
378-
379259
def __parse_request_range(range_header_text):
380260
""" Return a tuple describing the byte range requested in a GET request
381261
If the range is open ended on the left or right side, then a value of None
@@ -453,28 +333,3 @@ def next_stale_after_value(stale_after):
453333
return str(stal_after_count)
454334
except ValueError:
455335
return 'never'
456-
457-
458-
def digest_challenge_response(app, qop, algorithm, stale=False):
459-
response = app.make_response('')
460-
response.status_code = 401
461-
462-
# RFC2616 Section4.2: HTTP headers are ASCII. That means
463-
# request.remote_addr was originally ASCII, so I should be able to
464-
# encode it back to ascii. Also, RFC2617 says about nonces: "The
465-
# contents of the nonce are implementation dependent"
466-
nonce = H(b''.join([
467-
getattr(request, 'remote_addr', u'').encode('ascii'),
468-
b':',
469-
str(time.time()).encode('ascii'),
470-
b':',
471-
os.urandom(10)
472-
]), algorithm)
473-
opaque = H(os.urandom(10), algorithm)
474-
475-
auth = WWWAuthenticate("digest")
476-
auth.set_digest('[email protected]', nonce, opaque=opaque,
477-
qop=('auth', 'auth-int') if qop is None else (qop,), algorithm=algorithm)
478-
auth.stale = stale
479-
response.headers['WWW-Authenticate'] = auth.to_header()
480-
return response

atests/test_authentication.robot

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
*** Settings ***
22
Library RequestsLibrary
33
Library customAuthenticator.py
4+
Resource res_setup.robot
45
Variables secretvar.py
56

67
*** Test Cases ***
78
Get With Auth
89
[Tags] get get-cert
910
${auth}= Create List user passwd
10-
Create Session httpbin https://httpbin.org auth=${auth} verify=${CURDIR}${/}cacert.pem
11-
${resp}= GET On Session httpbin /basic-auth/user/passwd
11+
Create Session authsession ${HTTP_LOCAL_SERVER} auth=${auth}
12+
${resp}= GET On Session authsession /basic-auth/user/passwd
1213
Should Be Equal As Strings ${resp.status_code} 200
1314
Should Be Equal As Strings ${resp.json()['authenticated']} True
1415

1516
Get With Custom Auth
1617
[Tags] get
1718
${auth}= Get Custom Auth user passwd
18-
Create Custom Session httpbin https://httpbin.org auth=${auth} verify=${CURDIR}${/}cacert.pem
19-
${resp}= GET On Session httpbin /basic-auth/user/passwd
19+
Create Custom Session authsession ${HTTP_LOCAL_SERVER} auth=${auth}
20+
${resp}= GET On Session authsession /basic-auth/user/passwd
2021
Should Be Equal As Strings ${resp.status_code} 200
2122
Should Be Equal As Strings ${resp.json()['authenticated']} True
2223

2324
Get With Digest Auth
2425
[Tags] get get-cert
2526
${auth}= Create List user pass
2627
Create Digest Session
27-
... httpbin
28-
... https://httpbin.org
28+
... authsession
29+
... ${HTTP_LOCAL_SERVER}
2930
... auth=${auth}
3031
... debug=3
31-
... verify=${CURDIR}${/}cacert.pem
32-
${resp}= GET On Session httpbin /digest-auth/auth/user/pass
32+
${resp}= GET On Session authsession /digest-auth/auth/user/pass
3333
Should Be Equal As Strings ${resp.status_code} 200
3434
Should Be Equal As Strings ${resp.json()['authenticated']} True
3535

@@ -49,8 +49,8 @@ Get With Digest Auth with Robot Secrets
4949
... msg=robot version does not support secrets
5050
${auth}= Create List user ${SECRET_PASSWORD}
5151
Create Digest Session
52-
... httpbin
53-
... https://httpbin.org
52+
... authsession
53+
... ${HTTP_LOCAL_SERVER}
5454
... auth=${auth}
5555
... debug=3
5656
... verify=${CURDIR}${/}cacert.pem

atests/test_ssl_certs.robot

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ Library RequestsLibrary
55
*** Test Cases ***
66
Get HTTPS & Verify Cert
77
[Tags] get get-cert
8-
Create Session httpbin https://httpbin.org verify=True
9-
${resp}= GET On Session httpbin /get
8+
Create Session sslsession https://github.com verify=True
9+
${resp}= GET On Session sslsession /
1010
Should Be Equal As Strings ${resp.status_code} 200
1111

1212
Get HTTPS & Verify Cert with a CA bundle
1313
[Tags] get get-cert
14-
Create Session httpbin https://httpbin.org verify=${CURDIR}${/}cacert.pem
15-
${resp}= GET On Session httpbin /get
14+
Create Session sslsession https://github.com verify=${CURDIR}${/}cacert.pem
15+
${resp}= GET On Session sslsession /
1616
Should Be Equal As Strings ${resp.status_code} 200
1717

1818
Get HTTPS with Client Side Certificates
1919
[Tags] get get-cert
2020
@{client_certs}= Create List ${CURDIR}${/}clientcert.pem ${CURDIR}${/}clientkey.pem
21-
Create Client Cert Session crtsession https://server.cryptomix.com/secure client_certs=@{client_certs}
22-
${resp}= GET On Session crtsession /
21+
Create Client Cert Session sslsession https://server.cryptomix.com/secure client_certs=@{client_certs}
22+
${resp}= GET On Session sslsession /
2323
Should Be Equal As Strings ${resp.status_code} 200

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
Topic :: Software Development :: Testing
2222
"""[1:-1]
2323

24-
TEST_REQUIRE = ['robotframework>=3.2.1', 'pytest', 'flask', 'six', 'coverage', 'flake8']
24+
TEST_REQUIRE = ['robotframework>=3.2.1', 'pytest', 'flask', 'six', 'coverage', 'flake8', 'Flask-HTTPAuth==4.8.0']
2525

2626
VERSION = None
2727
version_file = join(dirname(abspath(__file__)), 'src', 'RequestsLibrary', 'version.py')

0 commit comments

Comments
 (0)