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=[


Reply via email to