~lazr-developers/lazr.authentication/trunk

« back to all changes in this revision

Viewing changes to src/lazr/authentication/wsgi.py

  • Committer: Jürgen Gmach
  • Date: 2021-11-05 14:27:41 UTC
  • Revision ID: juergen.gmach@canonical.com-20211105142741-crqrjqm3dhkcy9wd
Moved to git

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009 Canonical Ltd.  All rights reserved.
2
 
#
3
 
# This file is part of lazr.authenticate
4
 
#
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
8
 
# License.
9
 
#
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.
14
 
#
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/>.
17
 
 
18
 
"""WSGI middleware that handles the server side of HTTP authenticate."""
19
 
 
20
 
__metaclass__ = type
21
 
__all__ = [
22
 
    'AuthenticationMiddleware',
23
 
    'BasicAuthMiddleware',
24
 
    'OAuthMiddleware',
25
 
]
26
 
 
27
 
import base64
28
 
import re
29
 
try:
30
 
    from urllib.parse import urlunparse
31
 
except ImportError:
32
 
    from urlparse import urlunparse
33
 
 
34
 
from oauth.oauth import (
35
 
    OAuthError, OAuthRequest, OAuthServer, OAuthSignatureMethod_PLAINTEXT)
36
 
 
37
 
 
38
 
class AuthenticationMiddleware(object):
39
 
    """A base class for middleware that authenticates HTTP requests.
40
 
 
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.
45
 
 
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").
49
 
    """
50
 
 
51
 
    def __init__(self, application, authenticate_with,
52
 
                 realm="Restricted area", protect_path_pattern='.*'):
53
 
        """Constructor.
54
 
 
55
 
        :param application: A WSGI application.
56
 
 
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.
64
 
 
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.
70
 
        """
71
 
        self.application = application
72
 
        self.authenticate_with = authenticate_with
73
 
        self.realm = realm
74
 
        self.protect_path_pattern = re.compile(protect_path_pattern)
75
 
 
76
 
    def _unauthorized(self, start_response):
77
 
        """Short-circuit the request with a 401 error code."""
78
 
        start_response("401 Unauthorized",
79
 
                       [('WWW-Authenticate',
80
 
                         '%s realm="%s"' % (self.AUTH_SCHEME, self.realm))])
81
 
        return [b'401 Unauthorized']
82
 
 
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)
89
 
 
90
 
        try:
91
 
            credentials = self.getCredentialsFromEnvironment(environ)
92
 
        except ValueError:
93
 
            credentials = None
94
 
        if credentials is None:
95
 
            return self._unauthorized(start_response)
96
 
 
97
 
        authenticated_user = self.authenticate_with(*credentials)
98
 
        if authenticated_user is None:
99
 
            return self._unauthorized(start_response)
100
 
 
101
 
        environ['authenticated_user'] = authenticated_user
102
 
 
103
 
        return self.application(environ, start_response)
104
 
 
105
 
    def getCredentialsFromEnvironment(self, environment):
106
 
        """Retrieve a set of credentials from the environment.
107
 
 
108
 
        This superclass implementation ignores the environment
109
 
        entirely, and so never authenticates anybody.
110
 
 
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
114
 
                 be determined.
115
 
        """
116
 
        return None
117
 
 
118
 
 
119
 
class BasicAuthMiddleware(AuthenticationMiddleware):
120
 
    """WSGI middleware that implements HTTP Basic Auth."""
121
 
 
122
 
    AUTH_SCHEME = "Basic"
123
 
 
124
 
    def getCredentialsFromEnvironment(self, environ):
125
 
        authorization = environ.get('HTTP_AUTHORIZATION')
126
 
        if authorization is None:
127
 
            return None
128
 
 
129
 
        method, auth = authorization.split(' ', 1)
130
 
        if method.lower() != 'basic':
131
 
            return None
132
 
 
133
 
        auth = base64.b64decode(auth.strip().encode('ascii')).decode()
134
 
        username, password = auth.split(':', 1)
135
 
        return username, password
136
 
 
137
 
 
138
 
class OAuthMiddleware(AuthenticationMiddleware):
139
 
    """WSGI middleware that implements (part of) OAuth.
140
 
 
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
144
 
    access tokens.
145
 
    """
146
 
 
147
 
    AUTH_SCHEME = "OAuth"
148
 
 
149
 
    def __init__(self, application, authenticate_with, data_store=None,
150
 
                 realm="Restricted area", protect_path_pattern='.*'):
151
 
        """See `AuthenticationMiddleware.`
152
 
 
153
 
        :param data_store: An OAuthDataStore.
154
 
        """
155
 
        super(OAuthMiddleware, self).__init__(
156
 
            application, authenticate_with, realm, protect_path_pattern)
157
 
        self.data_store = data_store
158
 
 
159
 
    def getCredentialsFromEnvironment(self, environ):
160
 
        http_method = environ['REQUEST_METHOD']
161
 
 
162
 
        # Recreate the URL.
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)
173
 
        if request is None:
174
 
            return None
175
 
        server = OAuthServer(self.data_store)
176
 
        server.add_signature_method(OAuthSignatureMethod_PLAINTEXT())
177
 
        try:
178
 
            consumer, token, parameters = server.verify_request(request)
179
 
        except OAuthError:
180
 
            return None
181
 
        return consumer, token, parameters