summaryrefslogtreecommitdiff
path: root/oauthlib/oauth1/rfc5849/__init__.py
blob: b10ff9c6342ade36967fccaa192f78e3cb8668a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# -*- coding: utf-8 -*-
from __future__ import absolute_import

"""
oauthlib.oauth1.rfc5849
~~~~~~~~~~~~~~

This module is an implementation of various logic needed
for signing and checking OAuth 1.0 RFC 5849 requests.
"""

import logging
import urlparse
from urllib import urlencode

from oauthlib.common import Request
from . import parameters, signature, utils

SIGNATURE_HMAC = u"HMAC-SHA1"
SIGNATURE_RSA = u"RSA-SHA1"
SIGNATURE_PLAINTEXT = u"PLAINTEXT"
SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)

SIGNATURE_TYPE_AUTH_HEADER = u'AUTH_HEADER'
SIGNATURE_TYPE_QUERY = u'QUERY'
SIGNATURE_TYPE_BODY = u'BODY'


class Client(object):
    """A client used to sign OAuth 1.0 RFC 5849 requests"""
    def __init__(self, client_key,
            client_secret=None,
            resource_owner_key=None,
            resource_owner_secret=None,
            callback_uri=None,
            signature_method=SIGNATURE_HMAC,
            signature_type=SIGNATURE_TYPE_AUTH_HEADER,
            rsa_key=None, verifier=None):
        self.client_key = client_key
        self.client_secret = client_secret
        self.resource_owner_key = resource_owner_key
        self.resource_owner_secret = resource_owner_secret
        self.signature_method = signature_method
        self.signature_type = signature_type
        self.callback_uri = callback_uri
        self.rsa_key = rsa_key
        self.verifier = verifier

        if self.signature_method == SIGNATURE_RSA and self.rsa_key is None:
            raise ValueError('rsa_key is required when using RSA signature method.')

    def get_oauth_signature(self, request):
        """Get an OAuth signature to be used in signing a request
        """
        if self.signature_method == SIGNATURE_PLAINTEXT:
            # fast-path
            return signature.sign_plaintext(self.client_secret,
                self.resource_owner_secret)

        # XXX hack to make sure oauth params are included in the info
        # passed to collect_parameters below. Is there a cleaner way?
        uri, headers, body = self._render(request)

        collected_params = signature.collect_parameters(
            uri_query=urlparse.urlparse(uri).query,
            body=body,
            headers=headers)
        logging.debug("Collected params: {}".format(collected_params))

        normalized_params = signature.normalize_parameters(collected_params)
        normalized_uri = signature.normalize_base_string_uri(request.uri)
        logging.debug("Normalized params: {}".format(normalized_params))
        logging.debug("Normalized URI: {}".format(normalized_uri))

        base_string = signature.construct_base_string(request.http_method,
            normalized_uri, normalized_params)

        logging.debug("Base signing string: {}".format(base_string))

        if self.signature_method == SIGNATURE_HMAC:
            return signature.sign_hmac_sha1(base_string, self.client_secret,
                self.resource_owner_secret)
        elif self.signature_method == SIGNATURE_RSA:
            return signature.sign_rsa_sha1(base_string, self.rsa_key)
        else:
            return signature.sign_plaintext(self.client_secret,
                self.resource_owner_secret)

    def get_oauth_params(self):
        """Get the basic OAuth parameters to be used in generating a signature.
        """
        params = [
            (u'oauth_nonce', utils.generate_nonce()),
            (u'oauth_timestamp', utils.generate_timestamp()),
            (u'oauth_version', u'1.0'),
            (u'oauth_signature_method', self.signature_method),
            (u'oauth_consumer_key', self.client_key),
        ]
        if self.resource_owner_key:
            params.append((u'oauth_token', self.resource_owner_key))
        if self.callback_uri:
            params.append((u'oauth_callback', self.callback_uri))
        if self.verifier:
            params.append((u'oauth_verifier', self.verifier))

        return params

    def _render(self, request, formencode=False):
        """Render a signed request according to signature type

        Returns a 3-tuple containing the request URI, headers, and body.

        If the formencode argument is True and the body contains parameters, it
        is escaped and returned as a valid formencoded string.
        """
        # TODO what if there are body params on a header-type auth?
        # TODO what if there are query params on a body-type auth?

        uri, headers, body = request.uri, request.headers, request.body

        # TODO: right now these prepare_* methods are very narrow in scope--they
        # only affect their little thing. In some cases (for example, with
        # header auth) it might be advantageous to allow these methods to touch
        # other parts of the request, like the headers—so the prepare_headers
        # method could also set the Content-Type header to x-www-form-urlencoded
        # like the spec requires. This would be a fundamental change though, and
        # I'm not sure how I feel about it.
        if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
            headers = parameters.prepare_headers(request.oauth_params, request.headers)
        elif self.signature_type == SIGNATURE_TYPE_BODY and (body == [] or request.body_has_params):
            body = parameters.prepare_form_encoded_body(request.oauth_params, request.body)
            if formencode:
                body = urlencode(body)
            headers['Content-Type'] = u'application/x-www-form-urlencoded'
        elif self.signature_type == SIGNATURE_TYPE_QUERY:
            uri = parameters.prepare_request_uri_query(request.oauth_params, request.uri)
        else:
            raise ValueError('Unknown signature type specified.')

        return uri, headers, body

    def sign(self, uri, http_method=u'GET', body='', headers=None):
        """Sign a request

        Signs an HTTP request with the specified parts.

        Returns a 3-tuple of the signed request's URI, headers, and body.
        Note that http_method is not returned as it is unaffected by the OAuth
        signing process.

        The body argument may be a dict, a list of 2-tuples, or a formencoded
        string. If the body argument is not a formencoded string and/or the
        Content-Type header is not 'x-www-form-urlencoded', it will be
        returned verbatim as it is unaffected by the OAuth signing process.
        Attempting to sign a request with non-formencoded data using the
        OAuth body signature type is invalid and will raise an exception.

        If the body does contain parameters, it will be returned as a properly-
        formatted formencoded string.

        All string data MUST be unicode. This includes strings inside body
        dicts, for example.
        """
        # normalize request data
        request = Request(uri, http_method, body, headers)

        # sanity check
        content_type = request.headers.get('Content-Type', None)
        if content_type == 'application/x-www-form-urlencoded' and not request.body_has_params:
            raise ValueError("Headers indicate a formencoded body but body was not decodable.")

        # generate the basic OAuth parameters
        request.oauth_params = self.get_oauth_params()

        # generate the signature
        request.oauth_params.append((u'oauth_signature', self.get_oauth_signature(request)))

        # render the signed request and return it
        return self._render(request, formencode=True)


class Server(object):
    """A server used to verify OAuth 1.0 RFC 5849 requests"""
    def __init__(self, signature_method=SIGNATURE_HMAC, rsa_key=None):
        self.signature_method = signature_method
        self.rsa_key = rsa_key

    def get_client_secret(self, client_key):
        raise NotImplementedError("Subclasses must implement this function.")

    def get_resource_owner_secret(self, resource_owner_key):
        raise NotImplementedError("Subclasses must implement this function.")

    def get_signature_type_and_params(self, uri_query, headers, body):
        signature_types_with_oauth_params = filter(lambda s: s[1], (
            (SIGNATURE_TYPE_AUTH_HEADER, utils.filter_oauth_params(
                signature.collect_parameters(headers=headers,
                exclude_oauth_signature=False))),
            (SIGNATURE_TYPE_BODY, utils.filter_oauth_params(
                signature.collect_parameters(body=body,
                exclude_oauth_signature=False))),
            (SIGNATURE_TYPE_QUERY, utils.filter_oauth_params(
                signature.collect_parameters(uri_query=uri_query,
                exclude_oauth_signature=False))),
        ))

        if len(signature_types_with_oauth_params) > 1:
            raise ValueError('oauth_ params must come from only 1 signature type but were found in %s' % ', '.join(
                [s[0] for s in signature_types_with_oauth_params]))
        try:
            signature_type, params = signature_types_with_oauth_params[0]
        except IndexError:
            raise ValueError('oauth_ params are missing. Could not determine signature type.')

        return signature_type, dict(params)

    def check_client_key(self, client_key):
        raise NotImplementedError("Subclasses must implement this function.")

    def check_resource_owner_key(self, client_key, resource_owner_key):
        raise NotImplementedError("Subclasses must implement this function.")

    def check_timestamp_and_nonce(self, timestamp, nonce):
        raise NotImplementedError("Subclasses must implement this function.")

    def check_request_signature(self, uri, http_method=u'GET', body='',
            headers=None):
        """Check a request's supplied signature to make sure the request is
        valid.

        Servers should return HTTP status 400 if a ValueError exception
        is raised and HTTP status 401 on return value False.

        Per `section 3.2`_ of the spec.

        .. _`section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
        """
        headers = headers or {}
        signature_type = None
        # FIXME: urlparse does not return unicode!
        uri_query = urlparse.urlparse(uri).query

        signature_type, params = self.get_signature_type_and_params(uri_query,
            headers, body)

        # the parameters may not include duplicate oauth entries
        filtered_params = utils.filter_oauth_params(params)
        if len(filtered_params) != len(params):
            raise ValueError("Duplicate OAuth entries.")

        params = dict(params)
        request_signature = params.get(u'oauth_signature')
        client_key = params.get(u'oauth_consumer_key')
        resource_owner_key = params.get(u'oauth_token')
        nonce = params.get(u'oauth_nonce')
        timestamp = params.get(u'oauth_timestamp')
        callback_uri = params.get(u'oauth_callback')
        verifier = params.get(u'oauth_verifier')
        signature_method = params.get(u'oauth_signature_method')

        # ensure all mandatory parameters are present
        if not all((request_signature, client_key, nonce,
                    timestamp, signature_method)):
            raise ValueError("Missing OAuth parameters.")

        # if version is supplied, it must be "1.0"
        if u'oauth_version' in params and params[u'oauth_version'] != u'1.0':
            raise ValueError("Invalid OAuth version.")

        # signature method must be valid
        if not signature_method in SIGNATURE_METHODS:
            raise ValueError("Invalid signature method.")

        # ensure client key is valid
        if not self.check_client_key(client_key):
            return False

        # ensure resource owner key is valid and not expired
        if not self.check_resource_owner_key(client_key, resource_owner_key):
            return False

        # ensure the nonce and timestamp haven't been used before
        if not self.check_timestamp_and_nonce(timestamp, nonce):
            return False

        # FIXME: extract realm, then self.check_realm

        # oauth_client parameters depend on client chosen signature method
        # which may vary for each request, section 3.4
        # HMAC-SHA1 and PLAINTEXT share parameters
        if signature_method == SIGNATURE_RSA:
            oauth_client = Client(client_key,
                resource_owner_key=resource_owner_key,
                callback_uri=callback_uri,
                signature_method=signature_method,
                signature_type=signature_type,
                rsa_key=self.rsa_key, verifier=verifier)
        else:
            client_secret = self.get_client_secret(client_key)
            resource_owner_secret = self.get_resource_owner_secret(
                resource_owner_key)
            oauth_client = Client(client_key,
                client_secret=client_secret,
                resource_owner_key=resource_owner_key,
                resource_owner_secret=resource_owner_secret,
                callback_uri=callback_uri,
                signature_method=signature_method,
                signature_type=signature_type,
                verifier=verifier)

        request = Request(uri, http_method, body, headers)
        request.oauth_params = params

        client_signature = oauth_client.get_oauth_signature(request)

        # FIXME: use near constant time string compare to avoid timing attacks
        return client_signature == request_signature