blob: 8b827c60eb401e923454e57b1b3c22ee4418930c [file] [log] [blame]
Joe Gregorio67d77772010-09-01 16:45:45 -04001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc. All Rights Reserved.
4
5"""Utilities for OAuth.
6
7Utilities for making it easier to work with OAuth.
8"""
9
10__author__ = 'jcgregorio@google.com (Joe Gregorio)'
11
12import copy
Joe Gregorio845a5452010-09-08 13:50:34 -040013import httplib2
Joe Gregorio67d77772010-09-01 16:45:45 -040014import oauth2 as oauth
Joe Gregorio845a5452010-09-08 13:50:34 -040015import urllib
16import logging
Joe Gregorio67d77772010-09-01 16:45:45 -040017
18try:
19 from urlparse import parse_qs, parse_qsl
20except ImportError:
21 from cgi import parse_qs, parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040022
23
Joe Gregorio67d77772010-09-01 16:45:45 -040024class MissingParameter(Exception):
25 pass
26
Joe Gregorio845a5452010-09-08 13:50:34 -040027
28def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040029 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040030
31
Joe Gregorio67d77772010-09-01 16:45:45 -040032def _oauth_uri(name, discovery, params):
Joe Gregorio845a5452010-09-08 13:50:34 -040033 """Look up the OAuth UR from the discovery
34 document and add query parameters based on
35 params.
36
37 name - The name of the OAuth URI to lookup, one
38 of 'request', 'access', or 'authorize'.
39 discovery - Portion of discovery document the describes
40 the OAuth endpoints.
41 params - Dictionary that is used to form the query parameters
42 for the specified URI.
43 """
Joe Gregorio67d77772010-09-01 16:45:45 -040044 if name not in ['request', 'access', 'authorize']:
45 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040046 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040047 query = {}
48 for key in keys:
49 if key in params:
50 query[key] = params[key]
51 return discovery[name]['url'] + '?' + urllib.urlencode(query)
52
Joe Gregorio845a5452010-09-08 13:50:34 -040053
54class Credentials(object):
55 """Base class for all Credentials objects.
56
57 Subclasses must define an authorize() method
58 that applies the credentials to an HTTP transport.
59 """
60
61 def authorize(self, http):
62 """Take an httplib2.Http instance (or equivalent) and
63 authorizes it for the set of credentials, usually by
64 replacing http.request() with a method that adds in
65 the appropriate headers and then delegates to the original
66 Http.request() method.
67 """
68 _abstract()
69
70
71class OAuthCredentials(Credentials):
72 """Credentials object for OAuth 1.0a
73 """
74
75 def __init__(self, consumer, token, user_agent):
76 """
77 consumer - An instance of oauth.Consumer.
78 token - An instance of oauth.Token constructed with
79 the access token and secret.
80 user_agent - The HTTP User-Agent to provide for this application.
81 """
82 self.consumer = consumer
83 self.token = token
84 self.user_agent = user_agent
85
86 def authorize(self, http):
87 """
88 Args:
89 http - An instance of httplib2.Http
90 or something that acts like it.
91
92 Returns:
93 A modified instance of http that was passed in.
94
95 Example:
96
97 h = httplib2.Http()
98 h = credentials.authorize(h)
99
100 You can't create a new OAuth
101 subclass of httplib2.Authenication because
102 it never gets passed the absolute URI, which is
103 needed for signing. So instead we have to overload
104 'request' with a closure that adds in the
105 Authorization header and then calls the original version
106 of 'request()'.
107 """
108 request_orig = http.request
109 signer = oauth.SignatureMethod_HMAC_SHA1()
110
111 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400112 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400113 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
114 connection_type=None):
115 """Modify the request headers to add the appropriate
116 Authorization header."""
117 req = oauth.Request.from_consumer_and_token(
118 self.consumer, self.token, http_method=method, http_url=uri)
119 req.sign_request(signer, self.consumer, self.token)
120 if headers == None:
121 headers = {}
122 headers.update(req.to_header())
123 if 'user-agent' not in headers:
124 headers['user-agent'] = self.user_agent
125 return request_orig(uri, method, body, headers,
126 redirections, connection_type)
127
128 http.request = new_request
129 return http
130
131
132class FlowThreeLegged(object):
133 """Does the Three Legged Dance for OAuth 1.0a.
134 """
135
Joe Gregorio67d77772010-09-01 16:45:45 -0400136 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
137 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400138 """
139 discovery - Section of the API discovery document that describes
140 the OAuth endpoints.
141 consumer_key - OAuth consumer key
142 consumer_secret - OAuth consumer secret
143 user_agent - The HTTP User-Agent that identifies the application.
144 **kwargs - The keyword arguments are all optional and required
145 parameters for the OAuth calls.
146 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400147 self.discovery = discovery
148 self.consumer_key = consumer_key
149 self.consumer_secret = consumer_secret
150 self.user_agent = user_agent
151 self.params = kwargs
152 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400153 required = {}
154 for uriinfo in discovery.itervalues():
155 for name, value in uriinfo['parameters'].iteritems():
156 if value['required'] and not name.startswith('oauth_'):
157 required[name] = 1
158 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400159 if key not in self.params:
160 raise MissingParameter('Required parameter %s not supplied' % key)
161
Joe Gregorio845a5452010-09-08 13:50:34 -0400162 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400163 """Returns a URI to redirect to the provider.
164
Joe Gregorio845a5452010-09-08 13:50:34 -0400165 oauth_callback - Either the string 'oob' for a non-web-based application,
166 or a URI that handles the callback from the authorization
167 server.
168
169 If oauth_callback is 'oob' then pass in the
170 generated verification code to step2_exchange,
171 otherwise pass in the query parameters received
172 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400173 """
174 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
175 client = oauth.Client(consumer)
176
177 headers = {
178 'user-agent': self.user_agent,
179 'content-type': 'application/x-www-form-urlencoded'
180 }
181 body = urllib.urlencode({'oauth_callback': oauth_callback})
182 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400183
Joe Gregorio67d77772010-09-01 16:45:45 -0400184 resp, content = client.request(uri, 'POST', headers=headers,
185 body=body)
186 if resp['status'] != '200':
Joe Gregorio845a5452010-09-08 13:50:34 -0400187 logging.error('Failed to retrieve temporary authorization: %s' % content)
Joe Gregorio67d77772010-09-01 16:45:45 -0400188 raise Exception('Invalid response %s.' % resp['status'])
189
190 self.request_token = dict(parse_qsl(content))
191
192 auth_params = copy.copy(self.params)
193 auth_params['oauth_token'] = self.request_token['oauth_token']
194
Joe Gregorio845a5452010-09-08 13:50:34 -0400195 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400196
Joe Gregorio845a5452010-09-08 13:50:34 -0400197 def step2_exchange(self, verifier):
198 """Exhanges an authorized request token
199 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400200
Joe Gregorio845a5452010-09-08 13:50:34 -0400201 verifier - either the verifier token, or a dictionary
202 of the query parameters to the callback, which contains
203 the oauth_verifier.
204 """
205
206 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
207 verifier = verifier['oauth_verifier']
208
209 token = oauth.Token(
210 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400211 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400212 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400213 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
214 client = oauth.Client(consumer, token)
215
216 headers = {
217 'user-agent': self.user_agent,
218 'content-type': 'application/x-www-form-urlencoded'
219 }
220
221 uri = _oauth_uri('access', self.discovery, self.params)
222 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400223 if resp['status'] != '200':
224 logging.error('Failed to retrieve access token: %s' % content)
225 raise Exception('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400226
Joe Gregorio845a5452010-09-08 13:50:34 -0400227 oauth_params = dict(parse_qsl(content))
228 token = oauth.Token(
229 oauth_params['oauth_token'],
230 oauth_params['oauth_token_secret'])
231
232 return OAuthCredentials(consumer, token, self.user_agent)