1
# Copyright 2009 Canonical Ltd. All rights reserved.
3
# This file is part of lazr.authenticate
5
# lazr.authenticate is free software: you can redistribute it and/or
6
# modify it under the terms of the GNU Lesser General Public License
7
# as published by the Free Software Foundation, version 3 of the
10
# lazr.authenticate is distributed in the hope that it will be
11
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
12
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
# Lesser General Public License for more details.
15
# You should have received a copy of the GNU Lesser General Public License
16
# along with lazr.authenticate. If not, see <http://www.gnu.org/licenses/>.
18
"""WSGI middleware that handles the server side of HTTP authenticate."""
22
'AuthenticationMiddleware',
23
'BasicAuthMiddleware',
30
from urllib.parse import urlunparse
32
from urlparse import urlunparse
34
from oauth.oauth import (
35
OAuthError, OAuthRequest, OAuthServer, OAuthSignatureMethod_PLAINTEXT)
38
class AuthenticationMiddleware(object):
39
"""A base class for middleware that authenticates HTTP requests.
41
This class implements a generic HTTP authenticate workflow:
42
check whether the requested resource is protected, get credentials
43
from the WSGI environment, validate them (using a callback
44
function) and either allow or deny acces.
46
All subclasses must define the variable AUTH_SCHEME, which is the
47
HTTP authentication scheme (eg."Basic", "OAuth"). This string is sent
48
along with a HTTP response code of 401 ("Unauthorized").
51
def __init__(self, application, authenticate_with,
52
realm="Restricted area", protect_path_pattern='.*'):
55
:param application: A WSGI application.
57
:param authenticate_with: A callback function that takes some
58
number of credential arguemnts (the number and type
59
depends on the implementation of
60
getCredentialsFromEnvironment()) and returns an object
61
representing the authenticated user. If the credentials
62
are invalid or don't identify any existing user, the
63
function should return None.
65
:param realm: The string to give out as the authenticate realm.
66
:param protect_path_pattern: A regular expression string. URL
67
paths matching this string will be protected with the
68
authenticate method. URL paths not matching this string
69
can be accessed without authenticating.
71
self.application = application
72
self.authenticate_with = authenticate_with
74
self.protect_path_pattern = re.compile(protect_path_pattern)
76
def _unauthorized(self, start_response):
77
"""Short-circuit the request with a 401 error code."""
78
start_response("401 Unauthorized",
80
'%s realm="%s"' % (self.AUTH_SCHEME, self.realm))])
81
return [b'401 Unauthorized']
83
def __call__(self, environ, start_response):
84
"""Protect certain resources by checking auth credentials."""
85
path_info = environ.get('PATH_INFO', '/')
86
if not self.protect_path_pattern.match(path_info):
87
environ.setdefault('authenticated_user')
88
return self.application(environ, start_response)
91
credentials = self.getCredentialsFromEnvironment(environ)
94
if credentials is None:
95
return self._unauthorized(start_response)
97
authenticated_user = self.authenticate_with(*credentials)
98
if authenticated_user is None:
99
return self._unauthorized(start_response)
101
environ['authenticated_user'] = authenticated_user
103
return self.application(environ, start_response)
105
def getCredentialsFromEnvironment(self, environment):
106
"""Retrieve a set of credentials from the environment.
108
This superclass implementation ignores the environment
109
entirely, and so never authenticates anybody.
111
:param environment: The WSGI environment.
112
:return: A list of objects to be passed into the authenticate_with
113
callback function, or None if the credentials could not
119
class BasicAuthMiddleware(AuthenticationMiddleware):
120
"""WSGI middleware that implements HTTP Basic Auth."""
122
AUTH_SCHEME = "Basic"
124
def getCredentialsFromEnvironment(self, environ):
125
authorization = environ.get('HTTP_AUTHORIZATION')
126
if authorization is None:
129
method, auth = authorization.split(' ', 1)
130
if method.lower() != 'basic':
133
auth = base64.b64decode(auth.strip().encode('ascii')).decode()
134
username, password = auth.split(':', 1)
135
return username, password
138
class OAuthMiddleware(AuthenticationMiddleware):
139
"""WSGI middleware that implements (part of) OAuth.
141
This middleware only protects resources by making sure requests
142
are signed with a valid consumer and access token. It does not
143
help clients get request tokens or exchange request tokens for
147
AUTH_SCHEME = "OAuth"
149
def __init__(self, application, authenticate_with, data_store=None,
150
realm="Restricted area", protect_path_pattern='.*'):
151
"""See `AuthenticationMiddleware.`
153
:param data_store: An OAuthDataStore.
155
super(OAuthMiddleware, self).__init__(
156
application, authenticate_with, realm, protect_path_pattern)
157
self.data_store = data_store
159
def getCredentialsFromEnvironment(self, environ):
160
http_method = environ['REQUEST_METHOD']
163
url_scheme = environ['wsgi.url_scheme']
164
hostname = environ['HTTP_HOST']
165
path = environ['PATH_INFO']
166
query_string = environ.get('QUERY_STRING', '')
167
original_url = urlunparse(
168
(url_scheme, hostname, path, '', query_string, ''))
169
headers = {'Authorization' : environ.get('HTTP_AUTHORIZATION', '')}
170
request = OAuthRequest().from_request(
171
http_method, original_url, headers=headers,
172
query_string=query_string)
175
server = OAuthServer(self.data_store)
176
server.add_signature_method(OAuthSignatureMethod_PLAINTEXT())
178
consumer, token, parameters = server.verify_request(request)
181
return consumer, token, parameters