python-ucam-webauth documentation¶
Contents:
Quickstart¶
Using the flask decorator¶
import flask
from flask import Flask
from ucam_webauth.raven.flask_glue import AuthDecorator
# Werkzeug deduces the hostname from the 'Host' or
# 'X-Forwarded-Host' headers, so we need a whitelist
class R(flask.Request):
trusted_hosts = {'your-domain.com', 'www.your-domain.com'}
app = Flask(__name__)
app.request_class = R
app.secret_key = "a secret key"
auth_decorator = AuthDecorator(desc="My website")
@app.route("/some_url")
@auth_decorator
def my_view():
return "You are " + auth_decorator.principal
if __name__ == '__main__':
app.run()
Requiring all flask requests be authenticated¶
import flask
from flask import Flask
from ucam_webauth.raven.flask_glue import AuthDecorator
# Werkzeug deduces the hostname from the 'Host' or
# 'X-Forwarded-Host' headers, so we need a whitelist
class R(flask.Request):
trusted_hosts = {'your-domain.com', 'www.your-domain.com'}
app = Flask(__name__)
app.request_class = R
app.secret_key = "a secret key"
auth_decorator = AuthDecorator()
app.before_request(auth_decorator.before_request)
@app.route("/")
def home():
return "You are " + auth_decorator.principal
if __name__ == '__main__':
app.run()
Manual request building and response parsing¶
To create requests:
>>> from ucam_webauth.raven import Request, Response
>>> r = Request(url="http://host/response/path", desc="My website")
>>> print str(r)
https://raven.cam.ac.uk/auth/authenticate.html?url=http%3A%2F%2Fhost%2Fresponse%2Fpath&ver=3&desc=My+website
And parse responses:
>>> r = Response("3!200!!20130705T150000Z!1373000000-00000-00!"
"http%3A%2F%2Fhost%2Fpath!djr61!current!pwd!!"
"36000!!2!signature-omitted")
>>> r.success
True
>>> r.principal
"djr61"
>>> r.ptags
set(["current"])
Warning¶
You must check various properties of received responses. See Checking response values
Integrating with existing authentication or session management¶
from ucam_webauth import raven
from datetime import datetime
from flask import Flask, session, flash, url_for, redirect, abort, request
app = Flask(__name__)
app.secret_key = "a secret key"
@app.route("/")
def home():
return "<a href='{0}'>Log in</a>".format(url_for('login'))
@app.route("/login")
def login():
u = url_for("response", _external=True)
r = raven.Request(url=u)
return redirect(str(r))
@app.route("/response")
def response():
r = raven.Response(request.args["WLS-Response"])
# checking url, issue, iact and aauth is very important!
# Werkzeug deduces the hostname from the 'Host' or
# 'X-Forwarded-Host' headers, so we need a whitelist
request.trusted_hosts = {'www.your-domain.com', 'your-domain.com'}
if r.url != request.base_url:
print "Bad url"
abort(400)
issue_delta = (datetime.utcnow() - r.issue).total_seconds()
if not -5 < issue_delta < 15:
print "Bad issue"
abort(403)
if r.success:
# a no-op here, but important if you set iact or aauth
if not r.check_iact_aauth(None, None):
print "check_iact_aauth failed"
abort(403)
session["user"] = r.principal
return redirect(url_for("secrets"))
else:
return redirect(url_for("home"))
@app.route("/secrets")
def secrets():
if session.get("user", None) is None:
abort(401)
return "You are {0}".format(session["user"])
if __name__ == "__main__":
app.run(debug=True)
Warning¶
You must check various properties of received responses. See Checking response values
See also¶
The included simple_demo flask app serves as a far more comprehensive example, including:
- decorator usage
- integration with existing authentication (i.e., user is offered to log in via Raven or some other method)
- full Raven logout
- message flashing
Security¶
Checking response values¶
You must check the url, issue, auth and sso attributes of the response:
check that url matches the current URL being requested / is what you expect.
Not checking url will allow another evil website administrator to replay responses produced by Raven log-ins to her website to yours, thereby impersonating someone else. (Using params as a token (below) doesn’t help, since the attacker can obtain a matching (cookie, params) pair from you first, and then ask the victim to authenticate with params set to that value.)
Some frameworks, notably Werkzeug, deduce the current hostname from the Host or X-Forwarded-Host headers (with the latter taking precedence).
See also
werkzeug#609 and issue 5
This technique may be used to whitelist domains in Flask:
class R(flask.Request): trusted_hosts = {'www.danielrichman.co.uk'} app.request_class = R
Alternatively, you could sanitise Host and X-Forwarded-Host in your web-server.
If you might have query parameters in your url, you need to take care to handle negative respones from the WLS. See Response URL for “cancels”.
check issue is within an acceptable range of now
… lest someone replay an old response to log in again
check auth and sso match iact and aauth
see
ucam_webauth.Response.check_iact_aauth()
Not checking iact/aauth will allow those restrictions to be bypassed by crafting a custom request to the WLS.
Using params as a token¶
You might like to set a random nonce in the Request’s params, save a hashed (with secret salt) or signed copy in a cookie, and check that they match in the Response.
This is not a substitute for any of the checks above, but does make the WLS-Response values in your web server access logs useless.
ucam_webauth.flask_glue.AuthDecorator
does this.
Signing keys¶
The keys used by Raven to sign responses are included with python-ucam-webauth. I took care in retrieving them, however you should trust neither me nor the method by which you installed this package. You should check that the copies of the certificates you have are correct / match the files at the links below (and audit the code you’ve just installed, I guess).
pubkey2
from https://raven.cam.ac.uk/project/keys/pubkey901
from https://raven.cam.ac.uk/project/keys/demo_server/
Misc¶
Response URL for “cancels”¶
The short story is that when the WLS wants to send a “response” to the WAA, it takes the URL you provided in the request, adds a WLS-Response query parameter, and redirects the client to that URL.
Happily, it guarantees that this will be done by appending (?|&)WLS-Response=… to the URL (which means that this process is easy to undo, which is a necessary part of Checking response values).
However: while in version 3 it preserves any query parameters that were already in the request URL, in version 1 of the protocol it will not: that is, it deletes the query component before appending ?WLS-Response…. Furthermore, while the current version of the WLS appears to reply with version 3 upon success, if you click “cancel” then it will use version 1, presumably because of reasons.
The WLS does include in its response a copy of some of the request parameters, in particular, the return URL. It is possible to extract this from the response, and after inspecting WLS-Response, perform a redirect to it, recovering the deleted query parameters. The flask_glue does exactly this, and so hopefully you should not suffer problems on account of this behaviour.
Note that if you for some reason had the requirement that requests to a certain page need only be Raven authenticated if a certain query parameter is present, then something like this would not work correctly:
def my_before_request():
if "special" in request.args:
return flask_glue.before_request()
else:
return None
… since if a user clicks Cancel, the special query parameter would not be set, so the before_request function would run, and the response from the WLS would not be handled. Instead, something like this would be necessary:
def my_before_request():
if "special" in request.args or "WLS-Response" in request.args:
return flask_glue.before_request()
else:
return None
If you are not using the flask_glue, I suggest where possible just avoiding having significant query parameters on the URL that you use to perform Raven authentication, and then simply check that request.base_url matches the URL in the signed response. Otherwise, have a look at the implementation of flask_glue for inspiration.
python module documentation¶
ucam_webauth¶
The ucam_webauth module implements version 3 of the WAA to WLS protocol.
It is not set up to talk to a specific WAA (i.e., Raven), and subclassing
this modules’ classes is required to make it functional. In particular, you
probably want to use ucam_webauth.raven
.
The protocol is implemented as defined at https://raven.cam.ac.uk/project/waa2wls-protocol.txt at the time of writing (though that URL may have since been replaced with a newer version). A copy of wawa2wls-protocol.txt is included with python-raven, and more information can be found at https://raven.cam.ac.uk/project/.
- WAA
- A WAA is a “Web Application Agent” (i.e., an application using this module)
- WLS
- The “Web Login Service” (i.e., Raven)
-
ucam_webauth.
ATYPE_PWD
¶ -
ucam_webauth.
STATUS_SUCCESS
¶ -
ucam_webauth.
STATUS_CANCELLED
¶ -
ucam_webauth.
STATUS_NOATYPES
¶ -
ucam_webauth.
STATUS_UNSUPPORTED_VERSION
¶ -
ucam_webauth.
STATUS_BAD_REQUEST
¶ -
ucam_webauth.
STATUS_INTERACTION_REQUIRED
¶ -
ucam_webauth.
STATUS_WAA_NOT_AUTHORISED
¶ -
ucam_webauth.
STATUS_AUTHENTICATION_DECLINED
¶ AuthenticationType
andStatus
instances used as constants in requests and responsesThey compare equal with their corresponding integers (for status codes) and strings (for atypes).
-
ucam_webauth.
STATUS_CODES
¶ A dict mapping status.code (i.e., the integer status code) to the relevant status object
-
class
ucam_webauth.
AuthenticationType
(name, description)[source]¶ An Authentication Type
This class exists to create the
ucam_webauth.AUTH_PWD
constant.-
name
¶ the name by which Ucam-webauth knows it
-
description
¶ a sentence describing it
Note that comparing an
AuthenticationType
object with astr
(or anotherAuthenticationType
object) will compare thename
attribute only. Further,str(atype) == atype.name
.-
-
class
ucam_webauth.
Status
(code, name, description)[source]¶ A WLS response Status
-
code
¶ a (three digit) integer
-
name
¶ short name for the status
-
description
¶ description: a sentence describing the status
Note that comparing a
Status
object with an integer (or anotherStatus
object) will compare thecode
attribute only. Further, int(status_object) == status_object.code-
-
class
ucam_webauth.
Request
(url, desc=None, aauth=None, iact=None, msg=None, params=None, fail=None, encode_strings=True)[source]¶ A Request to the WLS
Parameters: - url (
str
) – a fully qualified URL; the user will be returned here (along with the Response as a query parameter) afterwards - desc (
str
) – optional description of the resource/website (encoding - see below) - aauth (
set
ofAuthenticationType
objects) – optional set of permissible authentication types; we require the user to use one of them (if empty, the WLS uses its default set) - iact (
True
,False
orNone
) – interaction required, forbidden or don’t care (respectively) - msg (
str
) – optional message explaining why authentication is required (encoding - see below) - params (
str
) – data, which is returned unaltered in theResponse
- fail (
bool
) – if True, and authentication fails, the WLS must show an error message and not redirect back to the WAA
All parameters are available as attributes as of Request object, once created.
-
iact
¶
True
: the user must re-authenticateFalse
: no interaction with the user is permitted (the request will only succeed if the user’s identity can be returned without interacting at all)None
(default): interacts if required
-
msg
¶ -
desc
¶ The ‘msg’ and ‘desc’ parameters are restricted to printable ASCII characters (0x20 - 0x7e). The WLS will convert ‘<’ and ‘>’ to ‘<’ and ‘>’ before using either string in HTML, preventing the inclusion of markup. However, it does not touch ‘&’, so HTML character and numeric entities may be used to represent other characters.
If encode_strings is
True
,&
will be escaped to&
, and non-ascii characters in msg and desc will be converted to their numeric entities.Otherwise, it is up to you to encode your strings. An error will be raised if msg or desc contain non-printable-ASCII characters.
-
params
¶ The ucam-webauth protocol does not specify any restrictions on the content of params. However, awful things may happen if you put arbitrary binary data in here. The Raven server appears to interpret non-ascii contents as latin-1, turn them into html entities in order to put them in a hidden HTML input element, then turn them back into (hopefully) the same binary data to be returned in the Response. As a result it outright rejects ‘params’ containing bytes below 0x20, and has the potential to go horribly wrong and land you in encoding hell.
Basically, you probably want to base64 params before giving it to a Request object.
- url (
-
class
ucam_webauth.
Response
(string)[source]¶ A Response from the WLS
Constructed by parsing string, the ‘encoded response string’ from the WLS.
The Response class has the following attributes, which must be set by subclassing it (see
raven.Response
):A
set
ofstr
objectsThe ptags attribute is set to this value if the version of the response is less than 3
-
keys
¶ A dict mapping key identifiers (kid) to a RSA public key (which must be an object with a
verify(digest, signature)
method that returns abool
)
A Response object has the following attributes:
Always present
-
ver
¶ response protocol version (
int
)
-
msg
¶ a text message describing the status of the authentication request, suitable for display to the end-user (
str
)
-
issue
¶ response creation time (
datetime
, timezone naive - the values are UTC)
-
id
¶ an “identifier” for the response. (
int
)The tuple (issue, id) is guaranteed to be unique
-
url
¶ the value of url supplied in the request, or equivalently, the URL to which this response was delivered (
str
)
-
success
¶ shorthand for
status == STATUS_SUCCESS
(bool
)
-
params
¶ a copy of params from the request (
str
)
-
signed
¶ whether the signature was present and has been verified (
bool
)Note that a present but invalid signature will produce an exception when parsed.
Present if authentication was successful, otherwise ``None``:
-
principal:
the authenticated identity of the user (
str
)
attributes or properties of the principal (
frozenset
ofstr
objects)
-
auth
¶ method of authentication used (
AuthenticationType
constant, orNone
)If authentication was not established by interaction (i.e., the client was already authenticated) then auth is
None
-
sso
¶ previous successful authentication types used (
frozenset
ofAuthenticationType
constants)sso will not be the empty set if auth is
None
Optional if authentication was successful, otherwise ``None``:
-
life
¶ remaining life of the user’s WLS session (
int
, in seconds)
Required if signed is True:
-
kid
¶ identifies the RSA key used to sign the request (
str
)
flask_glue
¶
This module provides glue to make using python-raven with Flask easy
-
class
ucam_webauth.flask_glue.
AuthDecorator
(desc=None, aauth=None, iact=None, msg=None, max_life=7200, use_wls_life=False, inactive_timeout=None, issue_bounds=(15, 5), require_principal=None, require_ptags=frozenset([u'current']), can_trust_request_host=False)[source]¶ An instance of this class decorates views to add authentication.
To use it, you’ll need to subclass it and set response_class, request_class and logout_url (see
raven.flask_glue.AuthDecorator
). Then:auth_decorator = AuthDecorator() # settings, e.g., desc="..." go here @app.route("/some_url") @auth_decorator def my_view(): return "You are " + auth_decorator.principal
Or to require users be authenticated for all views:
app.before_request(auth_decorator.before_request)
Note that since it uses flask.session, you’ll need to set
app.secret_key
.We need to be able to reliably determine the hostname of the current website. This is retrieved from
flask.Request.url
. By default, Werkzeug will respect the value of aX-Forwarded-Host
header, which means that a man-in-the-middle can have someone authenticate to their website, and forward the response from the WLS on to you. You must either setflask.Request.trusted_hosts
, for example like so:class R(flask.Request): trusted_hosts = {'www.danielrichman.co.uk'} app.request_class = R
… or sanitise both the Host header and the X-Forwarded-Host header in your web-server. If you choose the second option, set can_trust_request_host.
This tries to emulate the feel of applying mod_ucam_webauth to a file.
The decorator wraps the view in a function that calls
before_request()
first, calling the original view function if it does not return a redirect or abort.You may wish to catch the 401 and 403 aborts with
app.errorhandler
.The
principal
, theirptags
, theissue
andlife
from the WLS are available as attributes of theAuthDecorator
object (magic properties that retrieve the current values fromflask.session
). Further, the attributesexpires
andexpires_all
give information on when the ucam_webauth session will expire.For the desc, aauth, iact, msg parameters, see
ucam_webauth.Request
.Note that the max_life, use_wls_life and inactive_timeout parameters deal with the ucam_webauth session only; they only affect
flask.session["_ucam_webauth"]
. Flask’s session expiry, cookie lifetimes, etc. are independent.Parameters: - max_life (
int
(seconds) orNone
) – upper bound on how long a successful authentication can last before it expires and the user must reauthenticate - use_wls_life (
bool
) – should we lower the life of the session to the life reported by the WLS, if it is less than max_life? - inactive_timeout (
int
(seconds) orNone
) – expire the session if no request is processed via this decorator in inactive_timeout seconds - issue_bounds (
tuple
: (int
,int
) (seconds)) – a tuple, (lower, upper) - how close the issue (datetime that the WLS says the authentication happened at) must be to now (i.e., requirenow - lower < issue < now + upper
; this is a combination of two settings found in mod_ucam_webauth: clock skew and response timeout,issue_bounds=(clock_skew + response_timeout, clock_skew)
is equivalent) - require_principal (
set
ofstr
, orNone
) – require the principal to be in the set - require_ptags (
set
ofstr
, orNone
) – require the ptags to contain any string in require_ptags (i.e., non empty intersection) - can_trust_request_host (
bool
) – Can we trust the hostname inrequest.url
? (see Checking response values)
More complex customisation is possible:
override
check_authorised()
to do more complex checking than require_principal, require_ptags (note that this replaces checking require_principal, require_ptags)override
session_new()
The
AuthDecorator
only touchesflask.session["_ucam_webauth"]
. If you’ve saved other (important) things to the session object, you may want to clear them out when the state changes.You can do this by subclassing and overriding session_new. It is called whenever a response is received from the WLS, except if the response is a successful re-authentication after session expiry, with the same principal and ptags as before.
To log the user out, call
logout()
, which will clear the session state. Further,logout()
returns aflask.redirect()
to the Raven logout page. Be aware that the default flask session handlers are susceptible to replay attacks.POST requests: Since it will redirect to the WLS and back, the auth decorator will discard any POST data in the process. You may wish to either work around this (by subclassing and saving it somewhere before redirecting) or ensure that when it returns (with a GET request) to the URL, a sensible page is displayed (the form, or an error message).
-
__call__
(view_function)[source]¶ Wraps view_function with the auth decorator
(
AuthDecorator
objects are callable so that they can be used as function decorators.)Calling it returns a ‘wrapper’ view function that calls
request()
first.
-
principal
¶ The current principal, or
None
The current ptags, or
None
-
issue
¶ When the last WLS response was issued
issue is converted to a unix timestamp (
int
), rather than thedatetime
object used byucam_webauth.Response
. (issue isNone
if there is no current session.)
-
life
¶ life of the last WLS response (
int
seconds), orNone
-
last
¶ Time (
int
unix timestamp) of the last decorated request
-
expires
¶ When (
int
unix timestamp) the current auth. will expire
-
expires_all
¶ A list of all things that could cause the current auth. to expire
A list of (
str
,int
unix timestamp) tuples; (reason, when).reason will be one of “config max life”, “wls life” or “inactive”.
-
before_request
()[source]¶ The “main” method
- checks if there is a response from the WLS
- checks if the current URL matches that which the WLS said it redirected to (avoid an evil admin of another site replaying successful authentications)
- checks if
flask.session
is empty - if so, then we deduce that the user has cookies disabled, and must abort immediately with 403 Forbidden, or we will start a redirect loop - checks if params matches the token we set (and saved in
flask.session
) when redirecting to Raven - checks if the authentication method used is permitted by aauth and user-interaction respected iact - if not, abort with 400 Bad Request
- updates the state with the response: updating the principal, ptags and issue information if it was a success, or clearing them (but setting a flag - see below: 401 Authentication Required will be thrown after redirect) if it was a failure
- returns a redirect that removes
WLS-Response
fromrequest.args
- checks if the “response was an authentication failure” flag is set
in
flask.session
- if so, clears the flag and aborts with 401 Authentication Required - checks to see if we are authenticated (and the session hasn’t
expired)
- if not, returns a redirect that will sends the user to the WLS to authenticate
- checks to see if the principal / ptags are permitted
- if not, aborts with a 403 Forbidden
- updates the ‘last used’ time in the state (to implement inactive_timeout)
Returns
None
, if the request should proceed to the actual view function.- checks if there is a response from the WLS
Check if an authenticated user is authorised.
The default implementation requires the principal to be in the whitelist
require_principal
(if it is notNone
, in which case any principal is allowed) and the intersection ofrequire_ptags
and ptags to be non-empty (unlessrequire_ptags
isNone
, in which case any ptags (or no ptags at all) is permitted).Note that the default value of
require_ptags
inraven.flask_glue.AuthDecorator
is{"current"}
.
- max_life (
ucam_webauth.raven¶
Raven
The Raven module subclasses ucam_webauth.Request
and
ucam_webauth.Response
in order to use the Raven URLs and the Raven
response settings (default ptags and signing keys).
-
ucam_webauth.raven.
PUBKEY2
¶ The key used to verify responses, from https://raven.cam.ac.uk/project/keys/
-
ucam_webauth.raven.
RAVEN_AUTH
¶ The WLS’ authentication start page:
RAVEN_AUTH.format(quoted_query_string)
will produce a request
-
ucam_webauth.raven.
RAVEN_LOGOUT
¶ The WLS’ logout page: redirecting to this URL will log the user out of Raven completely.
-
class
ucam_webauth.raven.
Request
(url, desc=None, aauth=None, iact=None, msg=None, params=None, fail=None, encode_strings=True)[source]¶ ucam_webauth.Request
, configured for live RavenRefer to
ucam_webauth
for documentation.
-
class
ucam_webauth.raven.
Response
(string)[source]¶ ucam_webauth.Response
, configured for live RavenRefer to
ucam_webauth
for documentation.
flask_glue
¶
-
class
ucam_webauth.raven.flask_glue.
AuthDecorator
(desc=None, aauth=None, iact=None, msg=None, max_life=7200, use_wls_life=False, inactive_timeout=None, issue_bounds=(15, 5), require_principal=None, require_ptags=frozenset([u'current']), can_trust_request_host=False)[source]¶ ucam_webauth.flask_glue.AuthDecorator
, configured for live RavenRefer to
ucam_webauth.flask_glue
for documentation.-
request_class
¶ alias of
ucam_webauth.raven.Request
-
response_class
¶ alias of
ucam_webauth.raven.Response
-
logout_url
= u'https://raven.cam.ac.uk/auth/logout.html'¶
-
demoserver
¶
Raven Demo Server
Provides Request and Response subclasses (as in the raven module), except these use the settings of the Raven Demo Server, http://raven.cam.ac.uk/project/test-demo/
-
ucam_webauth.raven.demoserver.
PUBKEY901
¶ The key used to verify responses, from https://raven.cam.ac.uk/project/keys/demo_server/
-
ucam_webauth.raven.demoserver.
RAVEN_DEMO_AUTH
¶ The WLS’ authentication start page:
RAVEN_DEMO_AUTH.format(quoted_query_string)
will produce a request
-
ucam_webauth.raven.demoserver.
RAVEN_DEMO_LOGOUT
¶ The WLS’ logout page: redirecting to this URL will log the user out of Raven completely.
-
class
ucam_webauth.raven.demoserver.
Request
(url, desc=None, aauth=None, iact=None, msg=None, params=None, fail=None, encode_strings=True)[source]¶ ucam_webauth.Request
, configured for the Raven demo serverRefer to
ucam_webauth
for documentation.
-
class
ucam_webauth.raven.demoserver.
Response
(string)[source]¶ ucam_webauth.Response
, configured for the Raven demo serverRefer to
ucam_webauth
for documentation.