-
Notifications
You must be signed in to change notification settings - Fork 1
/
utils.py
177 lines (128 loc) · 5.83 KB
/
utils.py
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
from django.conf import settings
import base64
import hashlib
import hmac
import json
import logging
import time
import urllib
import urllib2
import urlparse
BASE_LINK = "https://graph.facebook.com"
def base64_urldecode(data):
# 1. Pad the encoded string with "+". See
# http://fi.am/entry/urlsafe-base64-encodingdecoding-in-two-lines/
data += "=" * (4 - (len(data) % 4) % 4)
return base64.urlsafe_b64decode(data)
def parse_signed_request(signed_request, secret):
""" When a user authenticates via the JavaScript SDK, an fbsr_ cookie() gets
set that can be used to validate the uid. The payload includes an
authorization code that requires an extra network request to Facebook to
retrieve the session key/access token."""
encoded_sig, payload = signed_request.split('.', 2)
sig = base64_urldecode(encoded_sig)
data = json.loads(base64_urldecode(payload))
if data.get('algorithm').upper() != 'HMAC-SHA256':
return None
else:
expected_sig = hmac.new(secret, msg=payload, digestmod=hashlib.sha256).digest()
if sig != expected_sig:
raise Exception('signature did not mismatch...possible forgery?')
return data
def get_app_token_helper(data=None):
if not data:
data = {}
try:
token_request = urllib.urlencode(data)
app_token = urllib2.urlopen(BASE_LINK + "/oauth/access_token?%s" % token_request).read()
except urllib2.HTTPError, e:
logging.debug("Exception trying to grab Facebook App token (%s)" % e)
return {}
# Returned dictionary should contain "access_token" and possibly "expires"
ret = urlparse.parse_qs(app_token)
for key in ret:
ret[key] = ret[key][0] # shouldn't be multiple values for a key
return ret
def get_access_token_from_code(code, redirect_url=None):
""" OAuth2 code to retrieve an application access token. """
data = {
'client_id': settings.FACEBOOK_APP_ID,
'client_secret': settings.FACEBOOK_SECRET_KEY,
'code': code
}
if redirect_url:
data['redirect_uri'] = redirect_url
else:
data['redirect_uri'] = ''
return get_app_token_helper(data)
def get_signed_fb_request(cookies, app_id, app_secret, fetch_tokens=False):
"""Backwards compatibility routine to create a cookie_response dictionary
that can be used throughout our app."""
cookie = cookies.get("fbsr_" + app_id, "")
if not cookie:
return None
data = parse_signed_request(signed_request=cookie, secret=app_secret)
# We explictly do not try to grab the user's access token unless we absolutely need to do so,
# since Facebook OAuth2 requires a separate server-side request to fetch that data.
if fetch_tokens:
logging.debug("fetching tokens in get_signed_fb_request")
data = get_access_tokens_from_signed_fb_request(data)
else:
# Use so that backwards compatible Python OAuth v1.0 that used get_user_from_cookie()
# to still use user_id.
if data and data.get('user_id'):
data['uid'] = data['user_id']
else:
raise Exception("Something unexpected happen with the signed request %s...no user_id got passed in?" % data)
return data
def get_access_tokens_from_signed_fb_request(data):
if data:
response = get_access_token_from_code(data.get('code', ''))
if len(response) > 0:
token_response = {}
if "expires" in response:
token_response['expires'] = response.get("expires")
token_response['fbsr_signed'] = True # for debugging purposes
token_response['access_token'] = response.get("access_token", "")
token_response['session_key'] = '' # FB has changed their token format; no longer can get back session_key
return token_response
return None
def decode_cookie_string(cookie_string):
# The decode_cookie_string is how Facebook's Connect Library pulls out
# data within its fbs_ cookies. They URL-encode the cookie value and then
# store it as another nested set of key/value pairs. One of them
# is base_domain=, which we need to clear the cookie. If you don't
# have the domain= parameter set in the cookie, you can't clear the cookie.
unquoted_cookie = urllib.unquote(cookie_string)
# A much better way -- http://atomized.org/2008/06/parsing-url-query-parameters-in-python/
try:
cookie_dict = dict([part.split('=') for part in unquoted_cookie.split('&')])
except ValueError:
logging.debug("Bad cookie parsing of %s" % (unquoted_cookie))
return cookie_dict
##########################################################
# Test routines to create a cookie and sign the payload.
##########################################################
def fb_mock_cookie(user_id):
payload_dict = {
u'issued_at': time.time(),
# One drawback of OAuth2 is that it's hard to mock a Facebook
# authorization code that is used to retrieve a token.
u'code': 'AUTH_CODE_FB_USUALLY_GIVES_US',
u'user_id': user_id,
}
# Sign the payload before appending the signature with our secret key
signed_request = fb_sign_payload(payload_dict, settings.FACEBOOK_SECRET_KEY)
return ('fbsr_%s' % settings.FACEBOOK_APP_ID, signed_request)
def fb_sign_payload(payload, app_secret):
"""
Taken from external/facebook.py to match the signature with our payload
(the data passed into request.COOKIES
"""
payload['algorithm'] = 'HMAC-SHA256'
json_encoded_payload = json.dumps(payload)
b64_payload = base64.urlsafe_b64encode(json_encoded_payload)
signature = hmac.new(key=settings.FACEBOOK_SECRET_KEY,
msg=b64_payload, digestmod=hashlib.sha256).digest()
b64_signature = base64.urlsafe_b64encode(signature)
return "%s.%s" % (b64_signature, b64_payload)