Skip to content

Commit

Permalink
Merge #458 (add application secret proof support).
Browse files Browse the repository at this point in the history
  • Loading branch information
martey committed Sep 1, 2019
2 parents 4b7d76a + 119eee1 commit 31159b4
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 8 deletions.
7 changes: 7 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ jobs:
. ~/venv/bin/activate
pip install coverage pygments
pip install -e .
- run:
name: install mock if running Python 2
command: |
. ~/venv/bin/activate
if [ $(python -c "import platform; print(platform.python_version_tuple()[0])") == "2" ]; then
pip install mock
fi;
- run:
name: run linting
command: |
Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ You can read more about `Facebook's Graph API here`_.
* ``proxies`` - A ``dict`` with proxy-settings that Requests should use.
`See Requests documentation`_.
* ``session`` - A `Requests Session object`_.
* ``app_secret`` - A ``string`` containing the secret key of your
app. If both ``access_token`` and ``app_secret`` are present this will be
used to compute an `application secret proof`_ that will be sent on every
API request.


.. _Read more about access tokens here: https://developers.facebook.com/docs/facebook-login/access-tokens
.. _See more here: http://docs.python-requests.org/en/latest/user/quickstart/#timeouts
.. _version of Facebook's Graph API to use: https://developers.facebook.com/docs/apps/changelog#versions
.. _See Requests documentation: http://www.python-requests.org/en/latest/user/advanced/#proxies
.. _Requests Session object: http://docs.python-requests.org/en/master/user/advanced/#session-objects
.. _application secret proof: https://developers.facebook.com/docs/graph-api/securing-requests

**Example**

Expand Down
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Version 3.2.0 (unreleased)
- Add support for Graph API versions 3.2 and 3.3.
- Remove support for Graph API versions 2.8 and 2.9.
- Change default Graph API version to 2.10.
- Add support for securing Graph API Calls with a proof based on the
application secret (#454).

Version 3.1.0 (2018-11-06)
==========================
Expand Down
30 changes: 22 additions & 8 deletions facebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def __init__(
version=None,
proxies=None,
session=None,
app_secret=None,
):
# The default version is only used if the version kwarg does not exist.
default_version = VALID_API_VERSIONS[0]
Expand All @@ -94,6 +95,7 @@ def __init__(
self.timeout = timeout
self.proxies = proxies
self.session = session or requests.Session()
self.app_secret_hmac = None

if version:
version_regex = re.compile("^\d\.\d{1,2}$")
Expand All @@ -114,6 +116,13 @@ def __init__(
else:
self.version = "v" + default_version

if app_secret and access_token:
self.app_secret_hmac = hmac.new(
app_secret.encode("ascii"),
msg=access_token.encode("ascii"),
digestmod=hashlib.sha256,
).hexdigest()

def get_permissions(self, user_id):
"""Fetches the permissions object from the graph."""
response = self.request(
Expand Down Expand Up @@ -264,15 +273,20 @@ def request(
if post_args is not None:
method = "POST"

# Add `access_token` to post_args or args if it has not already been
# included.
if self.access_token:
# Add `access_token` and app secret proof (`app_secret_hmac`) to
# post_args or args if they exist and have not already been included.
def _add_to_post_args_or_args(arg_name, arg_value):
# If post_args exists, we assume that args either does not exists
# or it does not need `access_token`.
if post_args and "access_token" not in post_args:
post_args["access_token"] = self.access_token
elif "access_token" not in args:
args["access_token"] = self.access_token
# or it does not need updating.
if post_args and arg_name not in post_args:
post_args[arg_name] = arg_value
elif arg_name not in args:
args[arg_name] = arg_value

if self.access_token:
_add_to_post_args_or_args("access_token", self.access_token)
if self.app_secret_hmac:
_add_to_post_args_or_args("appsecret_proof", self.app_secret_hmac)

try:
response = self.session.request(
Expand Down
117 changes: 117 additions & 0 deletions test/test_facebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
from urlparse import parse_qs, urlparse
from urllib import urlencode

try:
from unittest import mock
except ImportError:
import mock


class FacebookTestCase(unittest.TestCase):
"""
Expand Down Expand Up @@ -397,5 +402,117 @@ def test_get_user_permissions_nonexistant_user(self):
facebook.GraphAPI(token).get_permissions(1)


class AppSecretProofTestCase(FacebookTestCase):
"""Tests related to application secret proofs."""

PROOF = "4dad02ff1693df832f9c183fe400fc4f601360be06514acb4a73edb783eec345"

ACCESS_TOKEN = "abc123"
APP_SECRET = "xyz789"

def test_appsecret_proof_set(self):
"""
Verify that application secret proof is set when a GraphAPI object is
initialized with an application secret and access token.
"""
api = facebook.GraphAPI(
access_token=self.ACCESS_TOKEN, app_secret=self.APP_SECRET
)
self.assertEqual(api.app_secret_hmac, self.PROOF)

def test_appsecret_proof_no_access_token(self):
"""
Verify that no application secret proof is set when
a GraphAPI object is initialized with an application secret
and no access token.
"""
api = facebook.GraphAPI(app_secret=self.APP_SECRET)
self.assertEqual(api.app_secret_hmac, None)

def test_appsecret_proof_no_app_secret(self):
"""
Verify that no application secret proof is set when
a GraphAPI object is initialized with no application secret
and no access token.
"""
api = facebook.GraphAPI(access_token=self.ACCESS_TOKEN)
self.assertEqual(api.app_secret_hmac, None)

@mock.patch("requests.request")
def test_appsecret_proof_is_set_on_get_request(self, mock_request):
"""
Verify that no application secret proof is sent with
GET requests whena GraphAPI object is initialized
with an application secret and an access token.
"""
api = facebook.GraphAPI(
access_token=self.ACCESS_TOKEN, app_secret=self.APP_SECRET
)
mock_response = mock.Mock()
mock_response.headers = {"content-type": "json"}
mock_response.json.return_value = {}
mock_request.return_value = mock_response
api.session.request = mock_request
api.request("some-path")
mock_request.assert_called_once_with(
"GET",
"https://graph.facebook.com/some-path",
data=None,
files=None,
params={"access_token": "abc123", "appsecret_proof": self.PROOF},
proxies=None,
timeout=None,
)

@mock.patch("requests.request")
def test_appsecret_proof_is_set_on_post_request(self, mock_request):
"""
Verify that no application secret proof is sent with
POST requests when a GraphAPI object is initialized
with an application secret and an access token.
"""
api = facebook.GraphAPI(
access_token=self.ACCESS_TOKEN, app_secret=self.APP_SECRET
)
mock_response = mock.Mock()
mock_response.headers = {"content-type": "json"}
mock_response.json.return_value = {}
mock_request.return_value = mock_response
api.session.request = mock_request
api.request("some-path", method="POST")
mock_request.assert_called_once_with(
"POST",
"https://graph.facebook.com/some-path",
data=None,
files=None,
params={"access_token": "abc123", "appsecret_proof": self.PROOF},
proxies=None,
timeout=None,
)

@mock.patch("requests.request")
def test_missing_appsecret_proof_is_not_set_on_request(self, mock_request):
"""
Verify that no application secret proof is set if GraphAPI
object is initialized without an application secret.
"""
api = facebook.GraphAPI(access_token=self.ACCESS_TOKEN)
mock_response = mock.Mock()
mock_response.headers = {"content-type": "json"}
mock_response.json.return_value = {}
mock_request.return_value = mock_response
api.session.request = mock_request
api.request("some-path")
mock_request.assert_called_once_with(
"GET",
"https://graph.facebook.com/some-path",
data=None,
files=None,
params={"access_token": "abc123"},
proxies=None,
timeout=None,
)


if __name__ == "__main__":
unittest.main()

0 comments on commit 31159b4

Please sign in to comment.