Hello community, here is the log from the commit of package python-PyFxA for openSUSE:Factory checked in at 2018-05-15 10:14:30 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-PyFxA (Old) and /work/SRC/openSUSE:Factory/.python-PyFxA.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-PyFxA" Tue May 15 10:14:30 2018 rev:2 rq:606764 version:0.6.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-PyFxA/python-PyFxA.changes 2018-04-30 22:59:41.266821065 +0200 +++ /work/SRC/openSUSE:Factory/.python-PyFxA.new/python-PyFxA.changes 2018-05-15 10:33:43.807465056 +0200 @@ -1,0 +2,9 @@ +Sun May 13 10:04:02 UTC 2018 - antoine.belv...@opensuse.org + +- Update to version 0.6.0: + * Add support for PKCE challenge and response in the OAuth flow. + * Add ability to supply `keys_jwk` when starting an OAuth flow. + * Improve scope-matching logic based on new FxA testcases, + including handling of URL-format scopes. + +------------------------------------------------------------------- Old: ---- PyFxA-0.5.0.tar.gz New: ---- PyFxA-0.6.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-PyFxA.spec ++++++ --- /var/tmp/diff_new_pack.o2xDIa/_old 2018-05-15 10:33:44.451441405 +0200 +++ /var/tmp/diff_new_pack.o2xDIa/_new 2018-05-15 10:33:44.455441258 +0200 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %bcond_with test Name: python-PyFxA -Version: 0.5.0 +Version: 0.6.0 Release: 0 Summary: Firefox Accounts client library for Python License: MPL-2.0 ++++++ PyFxA-0.5.0.tar.gz -> PyFxA-0.6.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/CHANGES.txt new/PyFxA-0.6.0/CHANGES.txt --- old/PyFxA-0.5.0/CHANGES.txt 2018-01-11 22:10:54.000000000 +0100 +++ new/PyFxA-0.6.0/CHANGES.txt 2018-05-04 05:15:58.000000000 +0200 @@ -3,6 +3,15 @@ This document describes changes between each past release. +0.6.0 (2018-05-04) +================== + +- Add support for PKCE challenge and response in the OAuth flow. +- Add ability to supply `keys_jwk` when starting an OAuth flow. +- Improve scope-matching logic based on new FxA testcases, + including handling of URL-format scopes. + + 0.5.0 (2018-01-12) ================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/PKG-INFO new/PyFxA-0.6.0/PKG-INFO --- old/PyFxA-0.5.0/PKG-INFO 2018-01-11 22:13:47.000000000 +0100 +++ new/PyFxA-0.6.0/PKG-INFO 2018-05-04 05:16:16.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: PyFxA -Version: 0.5.0 +Version: 0.6.0 Summary: Firefox Accounts client library for Python Home-page: https://github.com/mozilla/PyFxA Author: Mozilla Services Author-email: services-...@mozilla.org License: MPLv2.0 -Description-Content-Type: UNKNOWN Description: =========================================================== PyFxA: Python library for interacting with Firefox Accounts =========================================================== @@ -314,6 +313,15 @@ This document describes changes between each past release. + 0.6.0 (2018-05-04) + ================== + + - Add support for PKCE challenge and response in the OAuth flow. + - Add ability to supply `keys_jwk` when starting an OAuth flow. + - Improve scope-matching logic based on new FxA testcases, + including handling of URL-format scopes. + + 0.5.0 (2018-01-12) ================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/PyFxA.egg-info/PKG-INFO new/PyFxA-0.6.0/PyFxA.egg-info/PKG-INFO --- old/PyFxA-0.5.0/PyFxA.egg-info/PKG-INFO 2018-01-11 22:13:46.000000000 +0100 +++ new/PyFxA-0.6.0/PyFxA.egg-info/PKG-INFO 2018-05-04 05:16:14.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: PyFxA -Version: 0.5.0 +Version: 0.6.0 Summary: Firefox Accounts client library for Python Home-page: https://github.com/mozilla/PyFxA Author: Mozilla Services Author-email: services-...@mozilla.org License: MPLv2.0 -Description-Content-Type: UNKNOWN Description: =========================================================== PyFxA: Python library for interacting with Firefox Accounts =========================================================== @@ -314,6 +313,15 @@ This document describes changes between each past release. + 0.6.0 (2018-05-04) + ================== + + - Add support for PKCE challenge and response in the OAuth flow. + - Add ability to supply `keys_jwk` when starting an OAuth flow. + - Improve scope-matching logic based on new FxA testcases, + including handling of URL-format scopes. + + 0.5.0 (2018-01-12) ================== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/__main__.py new/PyFxA-0.6.0/fxa/__main__.py --- old/PyFxA-0.5.0/fxa/__main__.py 2018-01-11 20:27:29.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/__main__.py 2018-05-02 02:42:50.000000000 +0200 @@ -90,6 +90,16 @@ required=False, default=DEFAULT_CLIENT_ID) + parser.add_argument('--client-secret', + help='Firefox Account OAuth client secret.', + dest='client_secret', + required=False) + + parser.add_argument('--use-pkce', + help='Whether to use PKCE in OAuth code grant flow.', + dest='use_pkce', + action='store_true') + parser.add_argument('--scopes', help='Firefox Account OAuth scopes.', dest='scopes', @@ -189,13 +199,18 @@ if s.strip()] client_id = args['client_id'] unblock_code = args['unblock_code'] + client_secret = args['client_secret'] + use_pkce = args['use_pkce'] logger.info('Generating the Bearer Token.') try: token = get_bearer_token(email, password, scopes, account_server_url, - oauth_server_url, client_id, + oauth_server_url, + client_id, + client_secret, + use_pkce, unblock_code) except ClientError as e: logger.error(e) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/_utils.py new/PyFxA-0.6.0/fxa/_utils.py --- old/PyFxA-0.5.0/fxa/_utils.py 2017-12-21 00:31:48.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/_utils.py 2018-05-04 05:12:55.000000000 +0200 @@ -71,10 +71,8 @@ :note: - Sub-scopes are expressed using semi-colons. - - A required sub-scope will always match if its root-scope is among those - provided (e.g. ``profile:avatar`` will match ``profile`` if provided). + The rules for parsing and matching scopes in FxA are documented at + https://github.com/mozilla/fxa-oauth-server/blob/master/docs/scopes.md :param provided: list of scopes provided for the current token. :param required: the scope required (e.g. by the application). @@ -83,23 +81,59 @@ if not isinstance(required, (list, tuple)): required = [required] - def split_subscope(s): - return tuple((s.split(':') + [None])[:2]) + for req in required: + if not any(_match_single_scope(prov, req) for prov in provided): + return False + + return True + - provided = set([split_subscope(p) for p in provided]) - required = set([split_subscope(r) for r in required]) +def _match_single_scope(provided, required): + if provided.startswith('https:'): + return _match_url_scope(provided, required) + else: + return _match_shortname_scope(provided, required) - root_provided = set([root for (root, sub) in provided]) - root_required = set([root for (root, sub) in required]) - if not root_required.issubset(root_provided): +def _match_shortname_scope(provided, required): + if required.startswith('https:'): return False + prov_names = provided.split(':') + req_names = required.split(':') + # If we require :write, it must be provided. + if req_names[-1] == 'write': + if prov_names[-1] != 'write': + return False + req_names.pop() + prov_names.pop() + elif prov_names[-1] == 'write': + prov_names.pop() + # Provided names must be a prefix of required names. + if len(prov_names) > len(req_names): + return False + for (p, r) in zip(prov_names, req_names): + if p != r: + return False + # It matches! + return True - for (root, sub) in required: - if (root, None) in provided: - provided.add((root, sub)) - return required.issubset(provided) +def _match_url_scope(provided, required): + if not required.startswith('https:'): + return False + # Pop the hash fragments + (prov_url, prov_hash) = (provided.rsplit('#', 1) + [None])[:2] + (req_url, req_hash) = (required.rsplit('#', 1) + [None])[:2] + # Provided URL must be a prefix of required. + if req_url != prov_url: + if not (req_url.startswith(prov_url + '/')): + return False + # If hash is provided, it must match that required. + if prov_hash: + if not req_hash or req_hash != prov_hash: + return False + # It matches! + return True class APIClient(object): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/oauth.py new/PyFxA-0.6.0/fxa/oauth.py --- old/PyFxA-0.5.0/fxa/oauth.py 2018-01-11 20:27:11.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/oauth.py 2018-05-03 07:12:02.000000000 +0200 @@ -3,6 +3,9 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. import json +import os +import base64 +import hashlib from six import string_types from six.moves.urllib.parse import urlparse, urlunparse, urlencode, parse_qs @@ -52,8 +55,16 @@ service=client_id ) + def get_client_metadata(self, client_id=None): + """Get the OAuth client metadata for a given client_id.""" + if client_id is None: + client_id = self.client_id + return self.apiclient.get("/client/{0}".format(client_id)) + def get_redirect_url(self, state="", redirect_uri=None, scope=None, - action=None, email=None, client_id=None): + action=None, email=None, client_id=None, + code_challenge=None, code_challenge_method=None, + access_type=None, keys_jwk=None): """Get the URL to redirect to to initiate the oauth flow.""" if client_id is None: client_id = self.client_id @@ -69,16 +80,26 @@ params["action"] = action if email is not None: params["email"] = email + if code_challenge is not None: + params["code_challenge"] = code_challenge + if code_challenge_method is not None: + params["code_challenge_method"] = code_challenge_method + if keys_jwk is not None: + params["keys_jwk"] = keys_jwk + if access_type is not None: + params["access_type"] = access_type query_str = urlencode(params) authorization_url = urlparse(self.server_url + "/authorization") return urlunparse(authorization_url._replace(query=query_str)) - def trade_code(self, code, client_id=None, client_secret=None): + def trade_code(self, code, client_id=None, client_secret=None, + code_verifier=None): """Trade the authentication code for a longer lived token. :param code: the authentication code from the oauth redirect dance. :param client_id: the string generated during FxA client registration. :param client_secret: the related secret string. + :param code_verifier: optional PKCE code verifier. :returns: a dict with user id and authorized scopes for this token. """ if client_id is None: @@ -89,17 +110,21 @@ body = { 'code': code, 'client_id': client_id, - 'client_secret': client_secret } + if client_secret is not None: + body["client_secret"] = client_secret + if code_verifier is not None: + body["code_verifier"] = code_verifier resp = self.apiclient.post(url, body) if 'access_token' not in resp: error_msg = 'access_token missing in OAuth response' raise OutOfProtocolError(error_msg) - return resp['access_token'] + return resp - def authorize_code(self, sessionOrAssertion, scope=None, client_id=None): + def authorize_code(self, sessionOrAssertion, scope=None, client_id=None, + code_challenge=None, code_challenge_method=None): """Trade an identity assertion for an oauth authorization code. This method takes an identity assertion for a user and uses it to @@ -107,13 +132,14 @@ traded for a full-blown oauth token. Note that the authorize_token() method does the same thing but skips - the intermediate step of using a short-lived code, and hence this - method is likely only useful for testing purposes. + the intermediate step of using a short-lived code. You should prefer + that method if the registered OAuth client_id has `canGrant` permission. :param sessionOrAssertion: an identity assertion for the target user, or an auth session to use to make one. :param scope: optional scope to be provided by the token. :param client_id: the string generated during FxA client registration. + :param code_challenge: optional PKCE code challenge. """ if client_id is None: client_id = self.client_id @@ -126,6 +152,9 @@ } if scope is not None: body["scope"] = scope + if code_challenge is not None: + body["code_challenge"] = code_challenge + body["code_challenge_method"] = code_challenge_method or "S256" resp = self.apiclient.post(url, body) if "redirect" not in resp: @@ -227,3 +256,20 @@ 'token': token } self.apiclient.post(url, body) + + def generate_pkce_challenge(self): + """Ramdomly generate parameters for a PKCE challenge. + + This method returns a two-tuple (challenge, response) where the first + item contains request parameters for a PKCE challenge, and the second + item contains the corresponding parameters for a verification. + """ + code_verifier = base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8').rstrip("=") + raw_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest() + code_challenge = base64.urlsafe_b64encode(raw_challenge).decode('utf-8').rstrip("=") + return ({ + "code_challenge": code_challenge, + "code_challenge_method": "S256", + }, { + "code_verifier": code_verifier, + }) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/tests/test_core.py new/PyFxA-0.6.0/fxa/tests/test_core.py --- old/PyFxA-0.5.0/fxa/tests/test_core.py 2018-01-11 22:09:46.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/tests/test_core.py 2018-05-02 02:42:50.000000000 +0200 @@ -278,7 +278,7 @@ cert_exp = browserid.utils.decode_json_bytes(cert.split(".")[1])["exp"] ttl = round(float(cert_exp - millis) / 1000) self.assertGreaterEqual(ttl, 2) - self.assertLessEqual(ttl, 6) + self.assertLessEqual(ttl, 30) def test_change_password(self): # Change the password. @@ -313,13 +313,13 @@ # Validate cert expiry ttl = round(float(cert['exp'] - millis) / 1000) - self.assertGreaterEqual(ttl, 1232) - self.assertLessEqual(ttl, 1236) + self.assertGreaterEqual(ttl, 1230) + self.assertLessEqual(ttl, 1260) # Validate assertion expiry ttl = round(float(assertion['exp'] - millis) / 1000) - self.assertGreaterEqual(ttl, 1232) - self.assertLessEqual(ttl, 1236) + self.assertGreaterEqual(ttl, 1230) + self.assertLessEqual(ttl, 1260) def test_get_identity_assertion_accepts_service(self): # We can't observe any side-effects of sending the service query param, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/tests/test_oauth.py new/PyFxA-0.6.0/fxa/tests/test_oauth.py --- old/PyFxA-0.5.0/fxa/tests/test_oauth.py 2017-03-03 05:09:47.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/tests/test_oauth.py 2018-05-04 05:12:55.000000000 +0200 @@ -54,9 +54,12 @@ body=body, content_type='application/json') - self.token = self.client.trade_code('1234') + self.tokens = self.client.trade_code('1234') self.response = responses.calls[0] + def _get_request_body(self): + return json.loads(_decoded(responses.calls[0].request.body)) + def test_reaches_server_on_token_url(self): self.assertEqual(self.response.request.url, 'https://server/v1/token') @@ -71,7 +74,7 @@ self.assertEqual(body, expected) def test_returns_access_token_given_by_server(self): - self.assertEqual(self.token, "yeah") + self.assertEqual(self.tokens["access_token"], "yeah") @responses.activate def test_raises_error_if_access_token_not_returned(self): @@ -92,15 +95,43 @@ body='{"access_token": "tokay"}', content_type='application/json') # As positional arguments. - token = self.client.trade_code('1234', 'abc', 'cake') - self.assertEqual(token, "tokay") + tokens = self.client.trade_code('1234', 'abc', 'cake2') + self.assertEqual(tokens, {"access_token": "tokay"}) + self.assertEqual(self._get_request_body(), { + 'client_id': 'abc', + 'client_secret': 'cake2', + 'code': '1234', + }) # As keyword arguments. - token = self.client.trade_code( + tokens = self.client.trade_code( code='1234', client_id='abc', - client_secret='cake' + client_secret='cake2' + ) + self.assertEqual(tokens, {"access_token": "tokay"}) + self.assertEqual(self._get_request_body(), { + 'client_id': 'abc', + 'client_secret': 'cake2', + 'code': '1234', + }) + + @responses.activate + def test_trade_token_can_take_pkce_verifier_as_argument(self): + responses.add(responses.POST, + 'https://server/v1/token', + body='{"access_token": "tokay"}', + content_type='application/json') + tokens = self.client.trade_code( + code='1234', + code_verifier='verifyme', ) - self.assertEqual(token, "tokay") + self.assertEqual(tokens, {"access_token": "tokay"}) + self.assertEqual(self._get_request_body(), { + 'client_id': 'abc', + 'client_secret': 'cake', + 'code': '1234', + 'code_verifier': 'verifyme', + }) class TestAuthClientVerifyCode(unittest.TestCase): @@ -194,12 +225,17 @@ scope="profile profile:email", action="signup", email="t...@example.com", + code_challenge="challenge", + code_challenge_method="S1234", + access_type="offline", + keys_jwk="MockJWK", )) server_url = urlparse(self.server_url) self.assertEqual(redirect_url.hostname, server_url.hostname) params = parse_qs(redirect_url.query, keep_blank_values=True) all_params = ["action", "email", "client_id", "redirect_uri", - "scope", "state"] + "scope", "state", "access_type", "code_challenge", + "code_challenge_method", "keys_jwk"] self.assertEqual(sorted(params.keys()), sorted(all_params)) self.assertEqual(params["client_id"][0], self.client.client_id) self.assertEqual(params["state"][0], "applicationstate") @@ -207,6 +243,10 @@ self.assertEqual(params["scope"][0], "profile profile:email") self.assertEqual(params["action"][0], "signup") self.assertEqual(params["email"][0], "t...@example.com") + self.assertEqual(params["code_challenge"][0], "challenge") + self.assertEqual(params["code_challenge_method"][0], "S1234") + self.assertEqual(params["access_type"][0], "offline") + self.assertEqual(params["keys_jwk"][0], "MockJWK") class TestAuthClientAuthorizeCode(unittest.TestCase): @@ -259,6 +299,25 @@ }) @responses.activate + def test_authorize_code_with_pkce_challenge(self): + assertion = "A_FAKE_ASSERTION" + challenge, verifier = self.client.generate_pkce_challenge() + self.assertEqual(sorted(challenge), + ["code_challenge", "code_challenge_method"]) + self.assertEqual(sorted(verifier), + ["code_verifier"]) + code = self.client.authorize_code(assertion, **challenge) + self.assertEquals(code, "qed") + req_body = json.loads(_decoded(responses.calls[0].request.body)) + self.assertEquals(req_body, { + "assertion": assertion, + "client_id": self.client.client_id, + "state": "x", + "code_challenge": challenge["code_challenge"], + "code_challenge_method": challenge["code_challenge_method"], + }) + + @responses.activate def test_authorize_code_with_session_object(self): session = mock.Mock() session.get_identity_assertion.return_value = "IDENTITY" @@ -381,6 +440,66 @@ self.assertFalse(scope_matches(['abc:xyz'], ['abc'])) self.assertFalse(scope_matches(['abc:xyz', 'def'], ['abc', 'def'])) + def test_published_test_vectors_for_valid_matches(self): + VALID_MATCHES = [ + ['profile:write', 'profile'], + ['profile', 'profile:email'], + ['profile:write', 'profile:email'], + ['profile:write', 'profile:email:write'], + ['profile:email:write', 'profile:email'], + ['profile profile:email:write', 'profile:email'], + ['profile profile:email:write', 'profile:display_name'], + ['profile https://identity.mozilla.com/apps/oldsync', 'profile'], + ['foo bar:baz', 'foo:dee'], + ['foo bar:baz', 'bar:baz'], + ['foo bar:baz', 'foo:mah:pa bar:baz:quux'], + ['profile https://identity.mozilla.com/apps/oldsync', + 'https://identity.mozilla.com/apps/oldsync'], + ['https://identity.mozilla.com/apps/oldsync', + 'https://identity.mozilla.com/apps/oldsync#read'], + ['https://identity.mozilla.com/apps/oldsync', + 'https://identity.mozilla.com/apps/oldsync/bookmarks'], + ['https://identity.mozilla.com/apps/oldsync', + 'https://identity.mozilla.com/apps/oldsync/bookmarks#read'], + ['https://identity.mozilla.com/apps/oldsync#read', + 'https://identity.mozilla.com/apps/oldsync/bookmarks#read'], + ['https://identity.mozilla.com/apps/oldsync#read profile', + 'https://identity.mozilla.com/apps/oldsync/bookmarks#read'] + ] + for (provided, required) in VALID_MATCHES: + self.assertTrue(scope_matches(provided.split(), required.split()), + '"{}" should match "{}"'.format(provided, required)) + + def test_published_test_vectors_for_invalid_matches(self): + INVALID_MATCHES = [ + ['profile:email:write', 'profile'], + ['profile:email:write', 'profile:write'], + ['profile:email', 'profile:display_name'], + ['profilebogey', 'profile'], + ['foo bar:baz', 'bar'], + ['profile:write', 'https://identity.mozilla.com/apps/oldsync'], + ['profile profile:email:write', 'profile:write'], + ['https', 'https://identity.mozilla.com/apps/oldsync'], + ['https://identity.mozilla.com/apps/oldsync', 'profile'], + ['https://identity.mozilla.com/apps/oldsync#read', + 'https://identity.mozila.com/apps/oldsync/bookmarks'], + ['https://identity.mozilla.com/apps/oldsync#write', + 'https://identity.mozila.com/apps/oldsync/bookmarks#read'], + ['https://identity.mozilla.com/apps/oldsync/bookmarks', + 'https://identity.mozila.com/apps/oldsync'], + ['https://identity.mozilla.com/apps/oldsync/bookmarks', + 'https://identity.mozila.com/apps/oldsync/passwords'], + ['https://identity.mozilla.com/apps/oldsyncer', + 'https://identity.mozila.com/apps/oldsync'], + ['https://identity.mozilla.com/apps/oldsync', + 'https://identity.mozila.com/apps/oldsyncer'], + ['https://identity.mozilla.org/apps/oldsync', + 'https://identity.mozila.com/apps/oldsync'] + ] + for (provided, required) in INVALID_MATCHES: + self.assertFalse(scope_matches(provided.split(), required.split()), + '"{}" should not match "{}"'.format(provided, required)) + class TestCachedClient(unittest.TestCase): server_url = TEST_SERVER_URL diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/tests/test_requests_auth_plugin.py new/PyFxA-0.6.0/fxa/tests/test_requests_auth_plugin.py --- old/PyFxA-0.5.0/fxa/tests/test_requests_auth_plugin.py 2018-01-11 20:27:29.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/tests/test_requests_auth_plugin.py 2018-05-02 02:42:51.000000000 +0200 @@ -127,13 +127,18 @@ "Authorization headers does not start with Bearer") core_client_patch.assert_called_with( - server_url="https://accounts.com/auth/v1") + server_url="https://accounts.com/auth/v1" + ) oauth_client_patch.assert_called_with( - server_url="https://oauth.com/oauth/v1") + "53210789456", + None, + server_url="https://oauth.com/oauth/v1" + ) - core_client_patch.return_value.login.return_value. \ - get_identity_assertion.assert_called_with( - "https://oauth.com/") + oauth_client_patch.return_value.authorize_token.assert_called_with( + core_client_patch.return_value.login.return_value, + "profile" + ) @mock.patch('fxa.core.Client', return_value=mocked_core_client()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/tests/test_tools.py new/PyFxA-0.6.0/fxa/tests/test_tools.py --- old/PyFxA-0.5.0/fxa/tests/test_tools.py 2016-11-29 05:47:40.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/tests/test_tools.py 2018-05-02 02:42:51.000000000 +0200 @@ -49,7 +49,9 @@ account_server_url="account_server_url", oauth_server_url="oauth_server_url") oauth_client().authorize_token.assert_called_with( - 'abcd', 'profile', '543210789456') + core_client.return_value.login.return_value, + 'profile' + ) class TestGetBrowserIDAssertion(unittest.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/tools/bearer.py new/PyFxA-0.6.0/fxa/tools/bearer.py --- old/PyFxA-0.5.0/fxa/tools/bearer.py 2018-01-11 20:27:29.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/tools/bearer.py 2018-05-02 02:42:51.000000000 +0200 @@ -2,7 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. from __future__ import absolute_import -from six.moves.urllib.parse import urlparse from fxa import core from fxa import oauth @@ -11,6 +10,8 @@ account_server_url=None, oauth_server_url=None, client_id=None, + client_secret=None, + use_pkce=False, unblock_code=None): message = None @@ -33,12 +34,24 @@ client = core.Client(server_url=account_server_url) session = client.login(email, password, unblock_code=unblock_code) - url = urlparse(oauth_server_url) - audience = "%s://%s/" % (url.scheme, url.netloc) + oauth_client = oauth.Client(client_id, client_secret, + server_url=oauth_server_url) + + # XXX TODO: we should be able to automaticaly choose the most + # direct route to getting a token, based on registered client + # metadata. Unfortunately the oauth-server doesn't (yet) expose + # client properties like `canGrant` and `isPublic`. + # print metadata + # metadata = oauth_client.get_client_metadata() + + scope = ' '.join(scopes) + if client_secret is None and not use_pkce: + token = oauth_client.authorize_token(session, scope) + else: + challenge = verifier = {} + if use_pkce: + (challenge, verifier) = oauth_client.generate_pkce_challenge() + code = oauth_client.authorize_code(session, scope, **challenge) + token = oauth_client.trade_code(code, **verifier) - bid_assertion = session.get_identity_assertion(audience) - oauth_client = oauth.Client(server_url=oauth_server_url) - token = oauth_client.authorize_token(bid_assertion, - ' '.join(scopes), - client_id) return token diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/fxa/tools/create_user.py new/PyFxA-0.6.0/fxa/tools/create_user.py --- old/PyFxA-0.5.0/fxa/tools/create_user.py 2018-01-11 20:27:11.000000000 +0100 +++ new/PyFxA-0.6.0/fxa/tools/create_user.py 2018-05-02 02:42:51.000000000 +0200 @@ -4,6 +4,7 @@ from __future__ import absolute_import import base64 import os +import re import hmac from fxa import core @@ -14,7 +15,7 @@ def create_new_fxa_account(fxa_user_salt=None, account_server_url=None, prefix="fxa", content_server_url=None): - if account_server_url and 'stage' in account_server_url: + if account_server_url and re.search('(dev)|(stage)', account_server_url): if not fxa_user_salt: fxa_user_salt = os.urandom(36) else: @@ -35,7 +36,7 @@ finally: return email, password else: - message = ("You are not using stage (%s), make sure your FxA test " - "account exists: %s" % (account_server_url, - content_server_url)) + message = ("You are not using dev or stage (%s), make sure your FxA " + "test account exists: %s" % (account_server_url, + content_server_url)) raise ValueError(message) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/setup.cfg new/PyFxA-0.6.0/setup.cfg --- old/PyFxA-0.5.0/setup.cfg 2018-01-11 22:13:47.000000000 +0100 +++ new/PyFxA-0.6.0/setup.cfg 2018-05-04 05:16:16.000000000 +0200 @@ -10,4 +10,5 @@ [egg_info] tag_build = tag_date = 0 +tag_svn_revision = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/PyFxA-0.5.0/setup.py new/PyFxA-0.6.0/setup.py --- old/PyFxA-0.5.0/setup.py 2018-01-11 22:11:14.000000000 +0100 +++ new/PyFxA-0.6.0/setup.py 2018-05-04 05:15:19.000000000 +0200 @@ -48,7 +48,7 @@ setup(name="PyFxA", - version='0.5.0', + version='0.6.0', description="Firefox Accounts client library for Python", long_description=README + "\n\n" + CHANGES, classifiers=[