Skip to content

Commit 98c9321

Browse files
committed
Merge branch 'cancel-fix' (closes #8)
2 parents 2d75952 + 76bc312 commit 98c9321

5 files changed

Lines changed: 130 additions & 11 deletions

File tree

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Contents:
88

99
quickstart
1010
security
11+
misc
1112
modules
1213

1314
Links

docs/source/misc.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Misc
2+
========
3+
4+
.. _cancel_url:
5+
6+
Response URL for "cancels"
7+
--------------------------
8+
9+
The short story is that when the WLS wants to send a "response" to the WAA, it
10+
takes the URL you provided in the request, adds a `WLS-Response` query
11+
parameter, and redirects the client to that URL.
12+
13+
Happily, it guarantees that this will be done by appending
14+
`(?|&)WLS-Response=...` to the URL (which means that this process is easy to
15+
undo, which is a necessary part of :ref:`checking-response-values`).
16+
17+
However: while in version 3 it preserves any query parameters that were already
18+
in the request URL, in version 1 of the protocol it will not: that is, it
19+
deletes the query component before appending `?WLS-Response...`. Furthermore,
20+
while the current version of the WLS appears to reply with version 3 upon
21+
success, if you click "cancel" then it will use version 1, presumably because
22+
of reasons.
23+
24+
The WLS does include in its response a copy of some of the request parameters,
25+
in particular, the return URL. It is possible to extract this from the
26+
response, and after inspecting WLS-Response, perform a redirect to it,
27+
recovering the deleted query parameters. The `flask_glue` does exactly this,
28+
and so hopefully you should not suffer problems on account of this behaviour.
29+
30+
Note that if you for some reason had the requirement that requests to a certain
31+
page need only be Raven authenticated if a certain query parameter is present,
32+
then something like this would not work correctly::
33+
34+
def my_before_request():
35+
if "special" in request.args:
36+
return flask_glue.before_request()
37+
else:
38+
return None
39+
40+
... since if a user clicks Cancel, the special query parameter would not be
41+
set, so the `before_request` function would run, and the response from the WLS
42+
would not be handled. Instead, something like this would be necessary::
43+
44+
def my_before_request():
45+
if "special" in request.args or "WLS-Response" in request.args:
46+
return flask_glue.before_request()
47+
else:
48+
return None
49+
50+
If you are not using the `flask_glue`, I suggest where possible just avoiding
51+
having significant query parameters on the URL that you use to perform Raven
52+
authentication, and then simply check that `request.base_url` matches the URL
53+
in the signed response. Otherwise, have a look at the implementation of
54+
`flask_glue` for inspiration.

docs/source/security.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ response:
3636
Alternatively, you could sanitise `Host` and `X-Forwarded-Host` in your
3737
web-server.
3838

39+
If you might have query parameters in your `url`, you need to take care to
40+
handle negative respones from the WLS. See :ref:`cancel_url`.
41+
3942
* check `issue` is within an acceptable range of *now*
4043

4144
... lest someone replay an old response to log in again

tests/test_auth_decorator.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,40 @@ def test_removes_wls_response_from_URL_401(self):
523523
r = client.get("/decorated")
524524
assert r.status_code == 401
525525

526+
# Sadly, the real world raven still uses version 1 for negative responses
527+
def test_handles_v1_removing_query_parameters(self):
528+
rig = TestRig()
529+
530+
with rig as (client, wls, views):
531+
r = client.get("/decorated?special")
532+
assert r.status_code == 303
533+
url, query = r.headers["location"].split("?")
534+
535+
with rig as (client, wls, views):
536+
u = "http://localhost/decorated?special"
537+
status = ucam_webauth.STATUS_CANCELLED
538+
wls.expect({"url": u}, make_response(ver=1, status=status, url=u))
539+
540+
r = client.get(url, query_string=query)
541+
assert r.status_code == 302
542+
url, query = r.headers["location"].split("?")
543+
assert url == "http://localhost/decorated"
544+
545+
# with a v1 response, the WLS actually deletes other query parameters
546+
assert query.startswith("special&WLS-Response")
547+
query = query[len("special&"):]
548+
549+
with rig as (client, wls, views):
550+
r = client.get("/decorated", query_string=query)
551+
assert r.status_code == 303
552+
# it should restore the query parameter:
553+
assert r.headers["location"] == "http://localhost/decorated?special"
554+
555+
with rig as (client, wls, views):
556+
# and finally give us a 401
557+
r = client.get("/decorated?special")
558+
assert r.status_code == 401
559+
526560
def test_require_default(self):
527561
self.check_auth(TestRig(), principal="something",
528562
ptags=set(["current"]))
@@ -632,10 +666,22 @@ def test_issue_bounds(self):
632666

633667
def test_checks_url(self):
634668
rig = TestRig()
669+
# Modify response.url: it should cause a mis-match
635670
self.check_auth_abort(rig, 400, url="http://localhost/other")
636671
self.check_auth_abort(rig, 400, url="http://other/decorated")
637672
self.check_auth_abort(rig, 400, url="http://localhost/decorated?test")
638673

674+
rig = TestRig()
675+
676+
with rig as (client, wls, views):
677+
# keep the response url as before, but modify the request url so
678+
# that they do not match
679+
url = "http://localhost/decorated?special"
680+
wls.expect({"url": url}, make_response(principal="djr61"))
681+
682+
response = client.get("/decorated?special", follow_redirects=True)
683+
assert response.status_code == 400
684+
639685
def test_doesnt_nuke_session(self):
640686
rig = TestRig()
641687

@@ -952,3 +998,9 @@ class R(flask.Request):
952998

953999
assert_raises(RuntimeError,
9541000
client.get, "/decorated", follow_redirects=True)
1001+
1002+
# Check that the v1 branch doesn't break anything.
1003+
# I'm tempted to say that a ver=1 successful response should be rejected, but
1004+
# I'm not sure.
1005+
def test_auth_v1(self):
1006+
self.check_auth(TestRig(), ver=1)

ucam_webauth/flask_glue.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def _handle_response(self, response):
398398
399399
"""
400400

401-
url_without_response = self._check_url(response.url)
401+
url_without_response = self._check_url(response)
402402
if url_without_response is None:
403403
abort(400)
404404

@@ -435,7 +435,7 @@ def _handle_response(self, response):
435435

436436
return redirect(url_without_response, code=303)
437437

438-
def _check_url(self, wls_response_url):
438+
def _check_url(self, response):
439439
"""
440440
Check if the response from the WLS was intended for us
441441
@@ -450,14 +450,16 @@ def _check_url(self, wls_response_url):
450450
"Host/X-Forwarded-Host headers and pass "
451451
"can_trust_request_host = True")
452452

453+
wls_response_url = response.url
453454
actual_url = request.url
454455

455-
# note: mod_ucam_webauth simply strips everything up to a ?
456-
# from both urls and compares.
456+
# See the docs (misc/Response URLs for Cancels) for comments on how
457+
# the WLS constructs the URL to which the user is redirected after a
458+
# request.
459+
# Essentially, it either appends (?|&)WLS-Response=... to the URL
460+
# in the request, or appends ?WLS-Response=... to the URL in the
461+
# request with the query part removed.
457462

458-
# see waa2wls-protocol.txt - the WLS appends (?|&)WLS-Response=
459-
# so, removing that from the end of the string should recover
460-
# the exact url sent in the request
461463
start = max(actual_url.rfind("?WLS-Response="),
462464
actual_url.rfind("&WLS-Response="))
463465
if start == -1:
@@ -475,13 +477,20 @@ def _check_url(self, wls_response_url):
475477
response_part, request.args["WLS-Response"])
476478
return None
477479

478-
# finally check that they agree.
480+
# chop off the WLS-Response
479481
actual_url = actual_url[:start]
480-
if wls_response_url != actual_url:
482+
483+
if response.ver == 1:
484+
expected_response_url, _, _ = wls_response_url.partition("?")
485+
else:
486+
expected_response_url = wls_response_url
487+
488+
# finally check that they agree.
489+
if expected_response_url != actual_url:
481490
logger.debug("response.url did not match url visited "
482491
"(replay of WLS-Response to other website?) "
483-
"response.url=%r request.url=%r",
484-
wls_response_url, actual_url)
492+
"expected_response_url=%r actual_url=%r",
493+
expected_response_url, actual_url)
485494
return None
486495

487496
return wls_response_url

0 commit comments

Comments
 (0)