Browse Source

HUE-8321 [useradmin] Integrate with Single Sign On using Keycloak
* adding mozilla-django-oidc-1.0.0 and josepy-1.1.0
* modified desktop/core/src/desktop/middleware.py to avoid redirect looping
* create OIDCBackend to extend OIDCAuthencationBackend, and it rewrites user with has_hue_permission
* adding entry for OIDC to Hue configure files and retrieve OIDC config value in desktop/settings.py

(cherry picked from commit d1801c9e0491666d0878a24d8d84a7e79b85bacf)

Ying Chen 7 years ago
parent
commit
9f5cf218ce
88 changed files with 6765 additions and 0 deletions
  1. 37 0
      desktop/conf.dist/hue.ini
  2. 37 0
      desktop/conf/pseudo-distributed.ini.tmpl
  3. 11 0
      desktop/core/ext-py/josepy-1.1.0/.coveragerc
  4. 18 0
      desktop/core/ext-py/josepy-1.1.0/.travis.yml
  5. 190 0
      desktop/core/ext-py/josepy-1.1.0/LICENSE.txt
  6. 5 0
      desktop/core/ext-py/josepy-1.1.0/MANIFEST.in
  7. 41 0
      desktop/core/ext-py/josepy-1.1.0/PKG-INFO
  8. 14 0
      desktop/core/ext-py/josepy-1.1.0/README.rst
  9. 1 0
      desktop/core/ext-py/josepy-1.1.0/docs/.gitignore
  10. 20 0
      desktop/core/ext-py/josepy-1.1.0/docs/Makefile
  11. 0 0
      desktop/core/ext-py/josepy-1.1.0/docs/_static/.gitignore
  12. 0 0
      desktop/core/ext-py/josepy-1.1.0/docs/_templates/.gitignore
  13. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/base64.rst
  14. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/errors.rst
  15. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/interfaces.rst
  16. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/json_util.rst
  17. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/jwa.rst
  18. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/jwk.rst
  19. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/jws.rst
  20. 5 0
      desktop/core/ext-py/josepy-1.1.0/docs/api/util.rst
  21. 1 0
      desktop/core/ext-py/josepy-1.1.0/docs/changelog.rst
  22. 191 0
      desktop/core/ext-py/josepy-1.1.0/docs/conf.py
  23. 21 0
      desktop/core/ext-py/josepy-1.1.0/docs/index.rst
  24. 8 0
      desktop/core/ext-py/josepy-1.1.0/docs/jws-help.txt
  25. 1 0
      desktop/core/ext-py/josepy-1.1.0/docs/man/jws.rst
  26. 1 0
      desktop/core/ext-py/josepy-1.1.0/docs/requirements.txt
  27. 4 0
      desktop/core/ext-py/josepy-1.1.0/pytest.ini
  28. 7 0
      desktop/core/ext-py/josepy-1.1.0/setup.cfg
  29. 113 0
      desktop/core/ext-py/josepy-1.1.0/setup.py
  30. 97 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/__init__.py
  31. 58 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/b64.py
  32. 76 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/b64_test.py
  33. 35 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/errors.py
  34. 17 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/errors_test.py
  35. 215 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/interfaces.py
  36. 114 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/interfaces_test.py
  37. 481 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/json_util.py
  38. 376 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/json_util_test.py
  39. 199 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/jwa.py
  40. 137 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/jwa_test.py
  41. 277 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/jwk.py
  42. 186 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/jwk_test.py
  43. 430 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/jws.py
  44. 233 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/jws_test.py
  45. 97 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/test_util.py
  46. 15 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/README
  47. 44 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert-100sans.pem
  48. 30 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert-idnsans.pem
  49. 14 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert-san.pem
  50. BIN
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert.der
  51. 13 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert.pem
  52. 28 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/critical-san.pem
  53. 41 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-100sans.pem
  54. 12 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-6sans.pem
  55. 27 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-idnsans.pem
  56. 8 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-nosans.pem
  57. 10 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-san.pem
  58. BIN
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr.der
  59. 10 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr.pem
  60. 14 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/dsa512_key.pem
  61. 15 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa1024_key.pem
  62. 22 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa2048_cert.pem
  63. 28 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa2048_key.pem
  64. 6 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa256_key.pem
  65. 9 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa512_key.pem
  66. 227 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/util.py
  67. 199 0
      desktop/core/ext-py/josepy-1.1.0/src/josepy/util_test.py
  68. 34 0
      desktop/core/ext-py/josepy-1.1.0/tox.ini
  69. 24 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/AUTHORS.rst
  70. 133 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/CONTRIBUTING.rst
  71. 146 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/HISTORY.rst
  72. 373 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/LICENSE
  73. 6 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/MANIFEST.in
  74. 365 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/PKG-INFO
  75. 189 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/README.rst
  76. 1 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/__init__.py
  77. 292 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/auth.py
  78. 139 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/middleware.py
  79. 26 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/urls.py
  80. 41 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/utils.py
  81. 195 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/views.py
  82. 18 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/setup.cfg
  83. 80 0
      desktop/core/ext-py/mozilla-django-oidc-1.0.0/setup.py
  84. 16 0
      desktop/core/src/desktop/auth/backend.py
  85. 90 0
      desktop/core/src/desktop/conf.py
  86. 7 0
      desktop/core/src/desktop/middleware.py
  87. 23 0
      desktop/core/src/desktop/settings.py
  88. 6 0
      desktop/core/src/desktop/urls.py

+ 37 - 0
desktop/conf.dist/hue.ini

@@ -343,6 +343,7 @@
     # - libsaml.backend.SAML2Backend
     # - libopenid.backend.OpenIDBackend
     # - liboauth.backend.OAuthBackend
+    # - desktop.auth.backend.OIDCBackend
     #     (New oauth, support Twitter, Facebook, Google+ and Linkedin
     # Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.
     # However, in order to enable OAuthBackend, it must be the ONLY backend configured.
@@ -718,6 +719,42 @@
     # The Authorize URL
     ## authenticate_url=https://api.twitter.com/oauth/authorize
 
+  # Configuration options for using OIDCBackend (Core) login for SSO
+  # ------------------------------------------------------------------------
+  [[oidc]]
+    # The client ID as relay party set in OpenID provider
+    ## oidc_rp_client_id=XXXXXXXXXXXXXXXXXXXXX
+
+    # The client secret as relay party set in OpenID provider
+    ## oidc_rp_client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+    # The OpenID provider authoriation endpoint
+    ## oidc_op_authorization_endpoint=https://keycloak.example.com/auth/realms/Cloudera/protocol/openid-connect/auth
+
+    # The OpenID provider token endpoint
+    ## oidc_op_token_endpoint=https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/token
+
+    # The OpenID provider user info endpoint
+    ## oidc_op_user_endpoint=https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/userinfo
+
+    # The OpenID provider signing key in PEM or DER format
+    ## oidc_rp_idp_sign_key=/path/to/key_file
+
+    # The OpenID provider authoriation endpoint
+    ## oidc_op_jwks_endpoint=https://keycloak.example.com/auth/realms/Cloudera/protocol/openid-connect/certs
+
+    # Whether Hue as OpenID Connect client verify SSL cert
+    ## oidc_verify_ssl=true
+
+    # As relay party Hue URL path to redirect to after login
+    ## login_redirect_url=https://localhost:8888/oidc/callback/
+
+    # The OpenID provider URL path to redirect to after logout
+    ## logout_redirect_url=https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/logout
+
+    # As relay party Hue URL path to redirect to after login
+    ## login_redirect_url_failure=https://localhost:8888/hue/oidc_failed/
+
   # Configuration options for Metrics
   # ------------------------------------------------------------------------
   [[metrics]]

+ 37 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -347,6 +347,7 @@
     # - libsaml.backend.SAML2Backend
     # - libopenid.backend.OpenIDBackend
     # - liboauth.backend.OAuthBackend
+    # - desktop.auth.backend.OIDCBackend
     #     (New oauth, support Twitter, Facebook, Google+ and Linkedin
     # Multiple Authentication backends are supported by specifying a comma-separated list in order of priority.
     # However, in order to enable OAuthBackend, it must be the ONLY backend configured.
@@ -720,6 +721,42 @@
     # The Authorize URL
     ## authenticate_url=https://api.twitter.com/oauth/authorize
 
+  # Configuration options for using OIDCBackend (Core) login for SSO
+  # ------------------------------------------------------------------------
+  [[oidc]]
+    # The client ID as relay party set in OpenID provider
+    ## oidc_rp_client_id=XXXXXXXXXXXXXXXXXXXXX
+
+    # The client secret as relay party set in OpenID provider
+    ## oidc_rp_client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+    # The OpenID provider authoriation endpoint
+    ## oidc_op_authorization_endpoint=https://keycloak.example.com/auth/realms/Cloudera/protocol/openid-connect/auth
+
+    # The OpenID provider token endpoint
+    ## oidc_op_token_endpoint=https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/token
+
+    # The OpenID provider user info endpoint
+    ## oidc_op_user_endpoint=https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/userinfo
+
+    # The OpenID provider signing key in PEM or DER format
+    ## oidc_rp_idp_sign_key=/path/to/key_file
+
+    # The OpenID provider authoriation endpoint
+    ## oidc_op_jwks_endpoint=https://keycloak.example.com/auth/realms/Cloudera/protocol/openid-connect/certs
+
+    # Whether Hue as OpenID Connect client verify SSL cert
+    ## oidc_verify_ssl=true
+
+    # As relay party Hue URL path to redirect to after login
+    ## login_redirect_url=https://localhost:8888/oidc/callback/
+
+    # The OpenID provider URL path to redirect to after logout
+    ## logout_redirect_url=https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/logout
+
+    # As relay party Hue URL path to redirect to after login
+    ## login_redirect_url_failure=https://localhost:8888/hue/oidc_failed/
+
   # Configuration options for Metrics
   # ------------------------------------------------------------------------
   [[metrics]]

+ 11 - 0
desktop/core/ext-py/josepy-1.1.0/.coveragerc

@@ -0,0 +1,11 @@
+[run]
+branch = True
+source = josepy
+
+[paths]
+source =
+   .tox/*/lib/python*/site-packages/josepy
+   .tox/pypy*/site-packages/josepy
+
+[report]
+show_missing = True

+ 18 - 0
desktop/core/ext-py/josepy-1.1.0/.travis.yml

@@ -0,0 +1,18 @@
+language: python
+sudo: false
+cache: pip
+python:
+  - "2.6"
+  - "2.7"
+  - "3.3"
+  - "3.4"
+  - "3.5"
+  - "3.6"
+install:
+  - pip install tox-travis codecov
+script:
+  - tox -v
+after_success:
+  - codecov
+notifications:
+  email: false

+ 190 - 0
desktop/core/ext-py/josepy-1.1.0/LICENSE.txt

@@ -0,0 +1,190 @@
+   Copyright 2015 Electronic Frontier Foundation and others
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/MANIFEST.in

@@ -0,0 +1,5 @@
+include LICENSE.txt
+include README.rst
+include .coveragerc .travis.yml pytest.ini tox.ini
+recursive-include docs *
+recursive-include src/josepy/testdata *

+ 41 - 0
desktop/core/ext-py/josepy-1.1.0/PKG-INFO

@@ -0,0 +1,41 @@
+Metadata-Version: 2.1
+Name: josepy
+Version: 1.1.0
+Summary: JOSE protocol implementation in Python
+Home-page: https://github.com/certbot/josepy
+Author: Certbot Project
+Author-email: client-dev@letsencrypt.org
+License: Apache License 2.0
+Description: JOSE protocol implementation in Python using cryptography
+        
+        .. image:: https://travis-ci.org/certbot/josepy.svg?branch=master
+          :target: https://travis-ci.org/certbot/josepy
+        
+        .. image:: https://codecov.io/gh/certbot/josepy/branch/master/graph/badge.svg
+          :target: https://codecov.io/gh/certbot/josepy
+        
+        .. image:: https://readthedocs.org/projects/josepy/badge/?version=latest
+          :target: http://josepy.readthedocs.io/en/latest/?badge=latest
+        
+        Originally developed as part of the ACME_ protocol implementation.
+        
+        .. _ACME: https://pypi.python.org/pypi/acme
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Security
+Provides-Extra: docs
+Provides-Extra: tests
+Provides-Extra: dev

+ 14 - 0
desktop/core/ext-py/josepy-1.1.0/README.rst

@@ -0,0 +1,14 @@
+JOSE protocol implementation in Python using cryptography
+
+.. image:: https://travis-ci.org/certbot/josepy.svg?branch=master
+  :target: https://travis-ci.org/certbot/josepy
+
+.. image:: https://codecov.io/gh/certbot/josepy/branch/master/graph/badge.svg
+  :target: https://codecov.io/gh/certbot/josepy
+
+.. image:: https://readthedocs.org/projects/josepy/badge/?version=latest
+  :target: http://josepy.readthedocs.io/en/latest/?badge=latest
+
+Originally developed as part of the ACME_ protocol implementation.
+
+.. _ACME: https://pypi.python.org/pypi/acme

+ 1 - 0
desktop/core/ext-py/josepy-1.1.0/docs/.gitignore

@@ -0,0 +1 @@
+/_build/

+ 20 - 0
desktop/core/ext-py/josepy-1.1.0/docs/Makefile

@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = python -msphinx
+SPHINXPROJ    = josepy
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

+ 0 - 0
desktop/core/ext-py/josepy-1.1.0/docs/_static/.gitignore


+ 0 - 0
desktop/core/ext-py/josepy-1.1.0/docs/_templates/.gitignore


+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/base64.rst

@@ -0,0 +1,5 @@
+JOSE Base64
+-----------
+
+.. automodule:: josepy.b64
+   :members:

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/errors.rst

@@ -0,0 +1,5 @@
+Errors
+------
+
+.. automodule:: josepy.errors
+   :members:

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/interfaces.rst

@@ -0,0 +1,5 @@
+Interfaces
+----------
+
+.. automodule:: josepy.interfaces
+   :members:

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/json_util.rst

@@ -0,0 +1,5 @@
+JSON utilities
+--------------
+
+.. automodule:: josepy.json_util
+   :members:

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/jwa.rst

@@ -0,0 +1,5 @@
+JSON Web Algorithms
+-------------------
+
+.. automodule:: josepy.jwa
+   :members:

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/jwk.rst

@@ -0,0 +1,5 @@
+JSON Web Key
+------------
+
+.. automodule:: josepy.jwk
+   :members:

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/jws.rst

@@ -0,0 +1,5 @@
+JSON Web Signature
+------------------
+
+.. automodule:: josepy.jws
+   :members:

+ 5 - 0
desktop/core/ext-py/josepy-1.1.0/docs/api/util.rst

@@ -0,0 +1,5 @@
+Utilities
+---------
+
+.. automodule:: josepy.util
+   :members:

+ 1 - 0
desktop/core/ext-py/josepy-1.1.0/docs/changelog.rst

@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst

+ 191 - 0
desktop/core/ext-py/josepy-1.1.0/docs/conf.py

@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+#
+# josepy documentation build configuration file, created by
+# sphinx-quickstart on Wed Oct 11 17:05:53 2017.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.todo',
+    'sphinx.ext.coverage',
+    'sphinx.ext.viewcode',
+]
+
+autodoc_member_order = 'bysource'
+autodoc_default_flags = ['show-inheritance', 'private-members']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'josepy'
+copyright = u"2015-2017, Let's Encrypt Project"
+author = u"Let's Encrypt Project"
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = u'1.1'
+# The full version, including alpha/beta/rc tags.
+release = u'1.1.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = True
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+default_role = 'py:obj'
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# This is required for the alabaster theme
+# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
+html_sidebars = {
+    '**': [
+        'about.html',
+        'navigation.html',
+        'relations.html',  # needs 'show_related': True theme option to display
+        'searchbox.html',
+        'donate.html',
+    ]
+}
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'josepydoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'josepy.tex', u'josepy Documentation',
+     u"Let's Encrypt Project", 'manual'),
+]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'josepy', u'josepy Documentation',
+     [author], 1),
+    ('man/jws', 'jws', u'jws script documentation', [project], 1),
+]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'josepy', u'josepy Documentation',
+     author, 'josepy', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+    'https://docs.python.org/': None,
+    'https://cryptography.io/en/latest/': None,
+}

+ 21 - 0
desktop/core/ext-py/josepy-1.1.0/docs/index.rst

@@ -0,0 +1,21 @@
+josepy
+======
+
+.. automodule:: josepy
+   :members:
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+   :glob:
+
+   api/*
+   changelog
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`

+ 8 - 0
desktop/core/ext-py/josepy-1.1.0/docs/jws-help.txt

@@ -0,0 +1,8 @@
+usage: jws [-h] [--compact] {sign,verify} ...
+
+positional arguments:
+  {sign,verify}
+
+optional arguments:
+  -h, --help     show this help message and exit
+  --compact

+ 1 - 0
desktop/core/ext-py/josepy-1.1.0/docs/man/jws.rst

@@ -0,0 +1 @@
+.. literalinclude:: ../jws-help.txt

+ 1 - 0
desktop/core/ext-py/josepy-1.1.0/docs/requirements.txt

@@ -0,0 +1 @@
+-e .[docs]

+ 4 - 0
desktop/core/ext-py/josepy-1.1.0/pytest.ini

@@ -0,0 +1,4 @@
+[pytest]
+addopts = -v --flake8 --cov-report xml --cov-report=term-missing --cov=josepy --cov-config .coveragerc
+norecursedirs = *.egg .eggs dist build docs .tox
+flake8-ignore = E501

+ 7 - 0
desktop/core/ext-py/josepy-1.1.0/setup.cfg

@@ -0,0 +1,7 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 113 - 0
desktop/core/ext-py/josepy-1.1.0/setup.py

@@ -0,0 +1,113 @@
+import io
+import sys
+
+from setuptools import find_packages, setup
+from setuptools.command.test import test as TestCommand
+
+version = '1.1.0'
+
+# Please update tox.ini when modifying dependency version requirements
+install_requires = [
+    # load_pem_private/public_key (>=0.6)
+    # rsa_recover_prime_factors (>=0.8)
+    'cryptography>=0.8',
+    # Connection.set_tlsext_host_name (>=0.13)
+    'PyOpenSSL>=0.13',
+    # For pkg_resources. >=1.0 so pip resolves it to a version cryptography
+    # will tolerate; see #2599:
+    'setuptools>=1.0',
+    'six>=1.9.0',  # needed for python_2_unicode_compatible
+]
+
+testing_requires = [
+    'coverage>=4.0',
+    'pytest-cache>=1.0',
+    'pytest-cov',
+    'flake8',
+    'pytest-flake8>=0.5',
+    'pytest>=2.8.0',
+    'mock',
+]
+
+# env markers cause problems with older pip and setuptools
+if sys.version_info < (2, 7):
+    install_requires.extend([
+        'argparse',
+        'ordereddict',
+    ])
+
+dev_extras = [
+    'pytest',
+    'tox',
+]
+
+docs_extras = [
+    'Sphinx>=1.0',  # autodoc_member_order = 'bysource', autodoc_default_flags
+    'sphinx_rtd_theme',
+]
+
+
+with io.open('README.rst', encoding='UTF-8') as f:
+    long_description = f.read()
+
+
+class PyTest(TestCommand):
+    user_options = []
+
+    def initialize_options(self):
+        TestCommand.initialize_options(self)
+        self.pytest_args = ''
+
+    def run_tests(self):
+        import shlex
+        # import here, cause outside the eggs aren't loaded
+        import pytest
+        errno = pytest.main(shlex.split(self.pytest_args))
+        sys.exit(errno)
+
+
+setup(
+    name='josepy',
+    version=version,
+    description='JOSE protocol implementation in Python',
+    long_description=long_description,
+    url='https://github.com/certbot/josepy',
+    author="Certbot Project",
+    author_email='client-dev@letsencrypt.org',
+    license='Apache License 2.0',
+    classifiers=[
+        'Development Status :: 3 - Alpha',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: Apache Software License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Topic :: Internet :: WWW/HTTP',
+        'Topic :: Security',
+    ],
+
+    packages=find_packages(where='src'),
+    package_dir={'': 'src'},
+    include_package_data=True,
+    install_requires=install_requires,
+    extras_require={
+        'dev': dev_extras,
+        'docs': docs_extras,
+        'tests': testing_requires,
+    },
+    entry_points={
+        'console_scripts': [
+            'jws = josepy.jws:CLI.run',
+        ],
+    },
+    tests_require=testing_requires,
+    cmdclass={
+        'test': PyTest,
+    },
+)

+ 97 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/__init__.py

@@ -0,0 +1,97 @@
+"""Javascript Object Signing and Encryption (JOSE).
+
+This package is a Python implementation of the standards developed by
+IETF `Javascript Object Signing and Encryption (Active WG)`_, in
+particular the following RFCs:
+
+- `JSON Web Algorithms (JWA)`_
+- `JSON Web Key (JWK)`_
+- `JSON Web Signature (JWS)`_
+
+Originally developed as part of the ACME_ protocol implementation.
+
+.. _`Javascript Object Signing and Encryption (Active WG)`:
+  https://tools.ietf.org/wg/jose/
+
+.. _`JSON Web Algorithms (JWA)`:
+  https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-algorithms/
+
+.. _`JSON Web Key (JWK)`:
+  https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-key/
+
+.. _`JSON Web Signature (JWS)`:
+  https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-signature/
+
+.. _ACME: https://pypi.python.org/pypi/acme
+
+"""
+import sys
+import warnings
+
+# flake8: noqa
+from josepy.b64 import (
+    b64decode,
+    b64encode,
+)
+
+from josepy.errors import (
+    DeserializationError,
+    SerializationError,
+    Error,
+    UnrecognizedTypeError,
+)
+
+from josepy.interfaces import JSONDeSerializable
+
+from josepy.json_util import (
+    Field,
+    JSONObjectWithFields,
+    TypedJSONObjectWithFields,
+    decode_b64jose,
+    decode_cert,
+    decode_csr,
+    decode_hex16,
+    encode_b64jose,
+    encode_cert,
+    encode_csr,
+    encode_hex16,
+)
+
+from josepy.jwa import (
+    HS256,
+    HS384,
+    HS512,
+    JWASignature,
+    PS256,
+    PS384,
+    PS512,
+    RS256,
+    RS384,
+    RS512,
+)
+
+from josepy.jwk import (
+    JWK,
+    JWKRSA,
+)
+
+from josepy.jws import (
+    Header,
+    JWS,
+    Signature,
+)
+
+from josepy.util import (
+    ComparableX509,
+    ComparableKey,
+    ComparableRSAKey,
+    ImmutableMap,
+)
+
+for (major, minor) in [(2, 6), (3, 3)]:
+    if sys.version_info[:2] == (major, minor):
+        warnings.warn(
+                "Python {0}.{1} support will be dropped in the next release of "
+                "josepy. Please upgrade your Python version.".format(major, minor),
+                DeprecationWarning,
+        )

+ 58 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/b64.py

@@ -0,0 +1,58 @@
+"""`JOSE Base64`_ is defined as:
+
+  - URL-safe Base64
+  - padding stripped
+
+.. _`JOSE Base64`:
+    https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-37#appendix-C
+
+.. Do NOT try to call this module "base64", as it will "shadow" the
+   standard library.
+
+"""
+import base64
+
+import six
+
+
+def b64encode(data):
+    """JOSE Base64 encode.
+
+    :param data: Data to be encoded.
+    :type data: bytes
+
+    :returns: JOSE Base64 string.
+    :rtype: bytes
+
+    :raises TypeError: if ``data`` is of incorrect type
+
+    """
+    if not isinstance(data, six.binary_type):
+        raise TypeError('argument should be {0}'.format(six.binary_type))
+    return base64.urlsafe_b64encode(data).rstrip(b'=')
+
+
+def b64decode(data):
+    """JOSE Base64 decode.
+
+    :param data: Base64 string to be decoded. If it's unicode, then
+                 only ASCII characters are allowed.
+    :type data: bytes or unicode
+
+    :returns: Decoded data.
+    :rtype: bytes
+
+    :raises TypeError: if input is of incorrect type
+    :raises ValueError: if input is unicode with non-ASCII characters
+
+    """
+    if isinstance(data, six.string_types):
+        try:
+            data = data.encode('ascii')
+        except UnicodeEncodeError:
+            raise ValueError(
+                'unicode argument should contain only ASCII characters')
+    elif not isinstance(data, six.binary_type):
+        raise TypeError('argument should be a str or unicode')
+
+    return base64.urlsafe_b64decode(data + b'=' * (4 - (len(data) % 4)))

+ 76 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/b64_test.py

@@ -0,0 +1,76 @@
+"""Tests for josepy.b64."""
+import unittest
+
+import six
+
+# https://en.wikipedia.org/wiki/Base64#Examples
+B64_PADDING_EXAMPLES = {
+    b'any carnal pleasure.': (b'YW55IGNhcm5hbCBwbGVhc3VyZS4', b'='),
+    b'any carnal pleasure': (b'YW55IGNhcm5hbCBwbGVhc3VyZQ', b'=='),
+    b'any carnal pleasur': (b'YW55IGNhcm5hbCBwbGVhc3Vy', b''),
+    b'any carnal pleasu': (b'YW55IGNhcm5hbCBwbGVhc3U', b'='),
+    b'any carnal pleas': (b'YW55IGNhcm5hbCBwbGVhcw', b'=='),
+}
+
+
+B64_URL_UNSAFE_EXAMPLES = {
+    six.int2byte(251) + six.int2byte(239): b'--8',
+    six.int2byte(255) * 2: b'__8',
+}
+
+
+class B64EncodeTest(unittest.TestCase):
+    """Tests for josepy.b64.b64encode."""
+
+    @classmethod
+    def _call(cls, data):
+        from josepy.b64 import b64encode
+        return b64encode(data)
+
+    def test_empty(self):
+        self.assertEqual(self._call(b''), b'')
+
+    def test_unsafe_url(self):
+        for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES):
+            self.assertEqual(self._call(text), b64)
+
+    def test_different_paddings(self):
+        for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES):
+            self.assertEqual(self._call(text), b64)
+
+    def test_unicode_fails_with_type_error(self):
+        self.assertRaises(TypeError, self._call, u'some unicode')
+
+
+class B64DecodeTest(unittest.TestCase):
+    """Tests for josepy.b64.b64decode."""
+
+    @classmethod
+    def _call(cls, data):
+        from josepy.b64 import b64decode
+        return b64decode(data)
+
+    def test_unsafe_url(self):
+        for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES):
+            self.assertEqual(self._call(b64), text)
+
+    def test_input_without_padding(self):
+        for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES):
+            self.assertEqual(self._call(b64), text)
+
+    def test_input_with_padding(self):
+        for text, (b64, pad) in six.iteritems(B64_PADDING_EXAMPLES):
+            self.assertEqual(self._call(b64 + pad), text)
+
+    def test_unicode_with_ascii(self):
+        self.assertEqual(self._call(u'YQ'), b'a')
+
+    def test_non_ascii_unicode_fails(self):
+        self.assertRaises(ValueError, self._call, u'\u0105')
+
+    def test_type_error_no_unicode_or_bytes(self):
+        self.assertRaises(TypeError, self._call, object())
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 35 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/errors.py

@@ -0,0 +1,35 @@
+"""JOSE errors."""
+
+
+class Error(Exception):
+    """Generic JOSE Error."""
+
+
+class DeserializationError(Error):
+    """JSON deserialization error."""
+
+    def __str__(self):
+        return "Deserialization error: {0}".format(
+            super(DeserializationError, self).__str__())
+
+
+class SerializationError(Error):
+    """JSON serialization error."""
+
+
+class UnrecognizedTypeError(DeserializationError):
+    """Unrecognized type error.
+
+    :ivar str typ: The unrecognized type of the JSON object.
+    :ivar jobj: Full JSON object.
+
+    """
+
+    def __init__(self, typ, jobj):
+        self.typ = typ
+        self.jobj = jobj
+        super(UnrecognizedTypeError, self).__init__(str(self))
+
+    def __str__(self):
+        return '{0} was not recognized, full message: {1}'.format(
+            self.typ, self.jobj)

+ 17 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/errors_test.py

@@ -0,0 +1,17 @@
+"""Tests for josepy.errors."""
+import unittest
+
+
+class UnrecognizedTypeErrorTest(unittest.TestCase):
+    def setUp(self):
+        from josepy.errors import UnrecognizedTypeError
+        self.error = UnrecognizedTypeError('foo', {'type': 'foo'})
+
+    def test_str(self):
+        self.assertEqual(
+            "foo was not recognized, full message: {'type': 'foo'}",
+            str(self.error))
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 215 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/interfaces.py

@@ -0,0 +1,215 @@
+"""JOSE interfaces."""
+import abc
+import collections
+import json
+
+import six
+
+from josepy import errors, util
+
+# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class
+# pylint: disable=too-few-public-methods
+
+
+@six.add_metaclass(abc.ABCMeta)
+class JSONDeSerializable(object):
+    # pylint: disable=too-few-public-methods
+    """Interface for (de)serializable JSON objects.
+
+    Please recall, that standard Python library implements
+    :class:`json.JSONEncoder` and :class:`json.JSONDecoder` that perform
+    translations based on respective :ref:`conversion tables
+    <conversion-table>` that look pretty much like the one below (for
+    complete tables see relevant Python documentation):
+
+    .. _conversion-table:
+
+    ======  ======
+     JSON   Python
+    ======  ======
+    object  dict
+    ...     ...
+    ======  ======
+
+    While the above **conversion table** is about translation of JSON
+    documents to/from the basic Python types only,
+    :class:`JSONDeSerializable` introduces the following two concepts:
+
+      serialization
+        Turning an arbitrary Python object into Python object that can
+        be encoded into a JSON document. **Full serialization** produces
+        a Python object composed of only basic types as required by the
+        :ref:`conversion table <conversion-table>`. **Partial
+        serialization** (accomplished by :meth:`to_partial_json`)
+        produces a Python object that might also be built from other
+        :class:`JSONDeSerializable` objects.
+
+      deserialization
+        Turning a decoded Python object (necessarily one of the basic
+        types as required by the :ref:`conversion table
+        <conversion-table>`) into an arbitrary Python object.
+
+    Serialization produces **serialized object** ("partially serialized
+    object" or "fully serialized object" for partial and full
+    serialization respectively) and deserialization produces
+    **deserialized object**, both usually denoted in the source code as
+    ``jobj``.
+
+    Wording in the official Python documentation might be confusing
+    after reading the above, but in the light of those definitions, one
+    can view :meth:`json.JSONDecoder.decode` as decoder and
+    deserializer of basic types, :meth:`json.JSONEncoder.default` as
+    serializer of basic types, :meth:`json.JSONEncoder.encode`  as
+    serializer and encoder of basic types.
+
+    One could extend :mod:`json` to support arbitrary object
+    (de)serialization either by:
+
+      - overriding :meth:`json.JSONDecoder.decode` and
+        :meth:`json.JSONEncoder.default` in subclasses
+
+      - or passing ``object_hook`` argument (or ``object_hook_pairs``)
+        to :func:`json.load`/:func:`json.loads` or ``default`` argument
+        for :func:`json.dump`/:func:`json.dumps`.
+
+    Interestingly, ``default`` is required to perform only partial
+    serialization, as :func:`json.dumps` applies ``default``
+    recursively. This is the idea behind making :meth:`to_partial_json`
+    produce only partial serialization, while providing custom
+    :meth:`json_dumps` that dumps with ``default`` set to
+    :meth:`json_dump_default`.
+
+    To make further documentation a bit more concrete, please, consider
+    the following imaginatory implementation example::
+
+      class Foo(JSONDeSerializable):
+          def to_partial_json(self):
+              return 'foo'
+
+          @classmethod
+          def from_json(cls, jobj):
+              return Foo()
+
+      class Bar(JSONDeSerializable):
+          def to_partial_json(self):
+              return [Foo(), Foo()]
+
+          @classmethod
+          def from_json(cls, jobj):
+              return Bar()
+
+    """
+
+    @abc.abstractmethod
+    def to_partial_json(self):  # pragma: no cover
+        """Partially serialize.
+
+        Following the example, **partial serialization** means the following::
+
+          assert isinstance(Bar().to_partial_json()[0], Foo)
+          assert isinstance(Bar().to_partial_json()[1], Foo)
+
+          # in particular...
+          assert Bar().to_partial_json() != ['foo', 'foo']
+
+        :raises josepy.errors.SerializationError:
+            in case of any serialization error.
+        :returns: Partially serializable object.
+
+        """
+        raise NotImplementedError()
+
+    def to_json(self):
+        """Fully serialize.
+
+        Again, following the example from before, **full serialization**
+        means the following::
+
+          assert Bar().to_json() == ['foo', 'foo']
+
+        :raises josepy.errors.SerializationError:
+            in case of any serialization error.
+        :returns: Fully serialized object.
+
+        """
+        def _serialize(obj):
+            if isinstance(obj, JSONDeSerializable):
+                return _serialize(obj.to_partial_json())
+            if isinstance(obj, six.string_types):  # strings are Sequence
+                return obj
+            elif isinstance(obj, list):
+                return [_serialize(subobj) for subobj in obj]
+            elif isinstance(obj, collections.Sequence):
+                # default to tuple, otherwise Mapping could get
+                # unhashable list
+                return tuple(_serialize(subobj) for subobj in obj)
+            elif isinstance(obj, collections.Mapping):
+                return dict((_serialize(key), _serialize(value))
+                            for key, value in six.iteritems(obj))
+            else:
+                return obj
+
+        return _serialize(self)
+
+    @util.abstractclassmethod
+    def from_json(cls, jobj):  # pylint: disable=unused-argument
+        """Deserialize a decoded JSON document.
+
+        :param jobj: Python object, composed of only other basic data
+            types, as decoded from JSON document. Not necessarily
+            :class:`dict` (as decoded from "JSON object" document).
+
+        :raises josepy.errors.DeserializationError:
+            if decoding was unsuccessful, e.g. in case of unparseable
+            X509 certificate, or wrong padding in JOSE base64 encoded
+            string, etc.
+
+        """
+        # TypeError: Can't instantiate abstract class <cls> with
+        # abstract methods from_json, to_partial_json
+        return cls()  # pylint: disable=abstract-class-instantiated
+
+    @classmethod
+    def json_loads(cls, json_string):
+        """Deserialize from JSON document string."""
+        try:
+            loads = json.loads(json_string)
+        except ValueError as error:
+            raise errors.DeserializationError(error)
+        return cls.from_json(loads)
+
+    def json_dumps(self, **kwargs):
+        """Dump to JSON string using proper serializer.
+
+        :returns: JSON document string.
+        :rtype: str
+
+        """
+        return json.dumps(self, default=self.json_dump_default, **kwargs)
+
+    def json_dumps_pretty(self):
+        """Dump the object to pretty JSON document string.
+
+        :rtype: str
+
+        """
+        return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
+
+    @classmethod
+    def json_dump_default(cls, python_object):
+        """Serialize Python object.
+
+        This function is meant to be passed as ``default`` to
+        :func:`json.dump` or :func:`json.dumps`. They call
+        ``default(python_object)`` only for non-basic Python types, so
+        this function necessarily raises :class:`TypeError` if
+        ``python_object`` is not an instance of
+        :class:`IJSONSerializable`.
+
+        Please read the class docstring for more information.
+
+        """
+        if isinstance(python_object, JSONDeSerializable):
+            return python_object.to_partial_json()
+        else:  # this branch is necessary, cannot just "return"
+            raise TypeError(repr(python_object) + ' is not JSON serializable')

+ 114 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/interfaces_test.py

@@ -0,0 +1,114 @@
+"""Tests for josepy.interfaces."""
+import unittest
+
+
+class JSONDeSerializableTest(unittest.TestCase):
+    # pylint: disable=too-many-instance-attributes
+
+    def setUp(self):
+        from josepy.interfaces import JSONDeSerializable
+
+        # pylint: disable=missing-docstring,invalid-name
+
+        class Basic(JSONDeSerializable):
+            def __init__(self, v):
+                self.v = v
+
+            def to_partial_json(self):
+                return self.v
+
+            @classmethod
+            def from_json(cls, jobj):
+                return cls(jobj)
+
+        class Sequence(JSONDeSerializable):
+            def __init__(self, x, y):
+                self.x = x
+                self.y = y
+
+            def to_partial_json(self):
+                return [self.x, self.y]
+
+            @classmethod
+            def from_json(cls, jobj):
+                return cls(
+                    Basic.from_json(jobj[0]), Basic.from_json(jobj[1]))
+
+        class Mapping(JSONDeSerializable):
+            def __init__(self, x, y):
+                self.x = x
+                self.y = y
+
+            def to_partial_json(self):
+                return {self.x: self.y}
+
+            @classmethod
+            def from_json(cls, jobj):
+                pass  # pragma: no cover
+
+        self.basic1 = Basic('foo1')
+        self.basic2 = Basic('foo2')
+        self.seq = Sequence(self.basic1, self.basic2)
+        self.mapping = Mapping(self.basic1, self.basic2)
+        self.nested = Basic([[self.basic1]])
+        self.tuple = Basic(('foo',))
+
+        # pylint: disable=invalid-name
+        self.Basic = Basic
+        self.Sequence = Sequence
+        self.Mapping = Mapping
+
+    def test_to_json_sequence(self):
+        self.assertEqual(self.seq.to_json(), ['foo1', 'foo2'])
+
+    def test_to_json_mapping(self):
+        self.assertEqual(self.mapping.to_json(), {'foo1': 'foo2'})
+
+    def test_to_json_other(self):
+        mock_value = object()
+        self.assertTrue(self.Basic(mock_value).to_json() is mock_value)
+
+    def test_to_json_nested(self):
+        self.assertEqual(self.nested.to_json(), [['foo1']])
+
+    def test_to_json(self):
+        self.assertEqual(self.tuple.to_json(), (('foo', )))
+
+    def test_from_json_not_implemented(self):
+        from josepy.interfaces import JSONDeSerializable
+        self.assertRaises(TypeError, JSONDeSerializable.from_json, 'xxx')
+
+    def test_json_loads(self):
+        seq = self.Sequence.json_loads('["foo1", "foo2"]')
+        self.assertTrue(isinstance(seq, self.Sequence))
+        self.assertTrue(isinstance(seq.x, self.Basic))
+        self.assertTrue(isinstance(seq.y, self.Basic))
+        self.assertEqual(seq.x.v, 'foo1')
+        self.assertEqual(seq.y.v, 'foo2')
+
+    def test_json_dumps(self):
+        self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
+
+    def test_json_dumps_pretty(self):
+        self.assertEqual(self.seq.json_dumps_pretty(),
+                         '[\n    "foo1",\n    "foo2"\n]')
+
+    def test_json_dump_default(self):
+        from josepy.interfaces import JSONDeSerializable
+
+        self.assertEqual(
+            'foo1', JSONDeSerializable.json_dump_default(self.basic1))
+
+        jobj = JSONDeSerializable.json_dump_default(self.seq)
+        self.assertEqual(len(jobj), 2)
+        self.assertTrue(jobj[0] is self.basic1)
+        self.assertTrue(jobj[1] is self.basic2)
+
+    def test_json_dump_default_type_error(self):
+        from josepy.interfaces import JSONDeSerializable
+        self.assertRaises(
+            TypeError, JSONDeSerializable.json_dump_default, object())
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 481 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/json_util.py

@@ -0,0 +1,481 @@
+"""JSON (de)serialization framework.
+
+The framework presented here is somewhat based on `Go's "json" package`_
+(especially the ``omitempty`` functionality).
+
+.. _`Go's "json" package`: http://golang.org/pkg/encoding/json/
+
+"""
+import abc
+import binascii
+import logging
+
+import OpenSSL
+import six
+
+from josepy import b64, errors, interfaces, util
+
+logger = logging.getLogger(__name__)
+
+
+class Field(object):
+    """JSON object field.
+
+    :class:`Field` is meant to be used together with
+    :class:`JSONObjectWithFields`.
+
+    ``encoder`` (``decoder``) is a callable that accepts a single
+    parameter, i.e. a value to be encoded (decoded), and returns the
+    serialized (deserialized) value. In case of errors it should raise
+    :class:`~josepy.errors.SerializationError`
+    (:class:`~josepy.errors.DeserializationError`).
+
+    Note, that ``decoder`` should perform partial serialization only.
+
+    :ivar str json_name: Name of the field when encoded to JSON.
+    :ivar default: Default value (used when not present in JSON object).
+    :ivar bool omitempty: If ``True`` and the field value is empty, then
+        it will not be included in the serialized JSON object, and
+        ``default`` will be used for deserialization. Otherwise, if ``False``,
+        field is considered as required, value will always be included in the
+        serialized JSON objected, and it must also be present when
+        deserializing.
+
+    """
+    __slots__ = ('json_name', 'default', 'omitempty', 'fdec', 'fenc')
+
+    def __init__(self, json_name, default=None, omitempty=False,
+                 decoder=None, encoder=None):
+        # pylint: disable=too-many-arguments
+        self.json_name = json_name
+        self.default = default
+        self.omitempty = omitempty
+
+        self.fdec = self.default_decoder if decoder is None else decoder
+        self.fenc = self.default_encoder if encoder is None else encoder
+
+    @classmethod
+    def _empty(cls, value):
+        """Is the provided value considered "empty" for this field?
+
+        This is useful for subclasses that might want to override the
+        definition of being empty, e.g. for some more exotic data types.
+
+        """
+        return not isinstance(value, bool) and not value
+
+    def omit(self, value):
+        """Omit the value in output?"""
+        return self._empty(value) and self.omitempty
+
+    def _update_params(self, **kwargs):
+        current = dict(json_name=self.json_name, default=self.default,
+                       omitempty=self.omitempty,
+                       decoder=self.fdec, encoder=self.fenc)
+        current.update(kwargs)
+        return type(self)(**current)  # pylint: disable=star-args
+
+    def decoder(self, fdec):
+        """Descriptor to change the decoder on JSON object field."""
+        return self._update_params(decoder=fdec)
+
+    def encoder(self, fenc):
+        """Descriptor to change the encoder on JSON object field."""
+        return self._update_params(encoder=fenc)
+
+    def decode(self, value):
+        """Decode a value, optionally with context JSON object."""
+        return self.fdec(value)
+
+    def encode(self, value):
+        """Encode a value, optionally with context JSON object."""
+        return self.fenc(value)
+
+    @classmethod
+    def default_decoder(cls, value):
+        """Default decoder.
+
+        Recursively deserialize into immutable types (
+        :class:`josepy.util.frozendict` instead of
+        :func:`dict`, :func:`tuple` instead of :func:`list`).
+
+        """
+        # bases cases for different types returned by json.loads
+        if isinstance(value, list):
+            return tuple(cls.default_decoder(subvalue) for subvalue in value)
+        elif isinstance(value, dict):
+            return util.frozendict(
+                dict((cls.default_decoder(key), cls.default_decoder(value))
+                     for key, value in six.iteritems(value)))
+        else:  # integer or string
+            return value
+
+    @classmethod
+    def default_encoder(cls, value):
+        """Default (passthrough) encoder."""
+        # field.to_partial_json() is no good as encoder has to do partial
+        # serialization only
+        return value
+
+
+class JSONObjectWithFieldsMeta(abc.ABCMeta):
+    """Metaclass for :class:`JSONObjectWithFields` and its subclasses.
+
+    It makes sure that, for any class ``cls`` with ``__metaclass__``
+    set to ``JSONObjectWithFieldsMeta``:
+
+    1. All fields (attributes of type :class:`Field`) in the class
+       definition are moved to the ``cls._fields`` dictionary, where
+       keys are field attribute names and values are fields themselves.
+
+    2. ``cls.__slots__`` is extended by all field attribute names
+       (i.e. not :attr:`Field.json_name`). Original ``cls.__slots__``
+       are stored in ``cls._orig_slots``.
+
+    In a consequence, for a field attribute name ``some_field``,
+    ``cls.some_field`` will be a slot descriptor and not an instance
+    of :class:`Field`. For example::
+
+      some_field = Field('someField', default=())
+
+      class Foo(object):
+          __metaclass__ = JSONObjectWithFieldsMeta
+          __slots__ = ('baz',)
+          some_field = some_field
+
+      assert Foo.__slots__ == ('some_field', 'baz')
+      assert Foo._orig_slots == ()
+      assert Foo.some_field is not Field
+
+      assert Foo._fields.keys() == ['some_field']
+      assert Foo._fields['some_field'] is some_field
+
+    As an implementation note, this metaclass inherits from
+    :class:`abc.ABCMeta` (and not the usual :class:`type`) to mitigate
+    the metaclass conflict (:class:`ImmutableMap` and
+    :class:`JSONDeSerializable`, parents of :class:`JSONObjectWithFields`,
+    use :class:`abc.ABCMeta` as its metaclass).
+
+    """
+
+    def __new__(mcs, name, bases, dikt):
+        fields = {}
+
+        for base in bases:
+            fields.update(getattr(base, '_fields', {}))
+        # Do not reorder, this class might override fields from base classes!
+        for key, value in tuple(six.iteritems(dikt)):
+            # not six.iterkeys() (in-place edit!)
+            if isinstance(value, Field):
+                fields[key] = dikt.pop(key)
+
+        dikt['_orig_slots'] = dikt.get('__slots__', ())
+        dikt['__slots__'] = tuple(
+            list(dikt['_orig_slots']) + list(six.iterkeys(fields)))
+        dikt['_fields'] = fields
+
+        return abc.ABCMeta.__new__(mcs, name, bases, dikt)
+
+
+@six.add_metaclass(JSONObjectWithFieldsMeta)
+class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
+    # pylint: disable=too-few-public-methods
+    """JSON object with fields.
+
+    Example::
+
+      class Foo(JSONObjectWithFields):
+          bar = Field('Bar')
+          empty = Field('Empty', omitempty=True)
+
+          @bar.encoder
+          def bar(value):
+              return value + 'bar'
+
+          @bar.decoder
+          def bar(value):
+              if not value.endswith('bar'):
+                  raise errors.DeserializationError('No bar suffix!')
+              return value[:-3]
+
+      assert Foo(bar='baz').to_partial_json() == {'Bar': 'bazbar'}
+      assert Foo.from_json({'Bar': 'bazbar'}) == Foo(bar='baz')
+      assert (Foo.from_json({'Bar': 'bazbar', 'Empty': '!'})
+              == Foo(bar='baz', empty='!'))
+      assert Foo(bar='baz').bar == 'baz'
+
+    """
+
+    @classmethod
+    def _defaults(cls):
+        """Get default fields values."""
+        return dict([(slot, field.default) for slot, field
+                     in six.iteritems(cls._fields)])
+
+    def __init__(self, **kwargs):
+        # pylint: disable=star-args
+        super(JSONObjectWithFields, self).__init__(
+            **(dict(self._defaults(), **kwargs)))
+
+    def encode(self, name):
+        """Encode a single field.
+
+        :param str name: Name of the field to be encoded.
+
+        :raises errors.SerializationError: if field cannot be serialized
+        :raises errors.Error: if field could not be found
+
+        """
+        try:
+            field = self._fields[name]
+        except KeyError:
+            raise errors.Error("Field not found: {0}".format(name))
+
+        return field.encode(getattr(self, name))
+
+    def fields_to_partial_json(self):
+        """Serialize fields to JSON."""
+        jobj = {}
+        omitted = set()
+        for slot, field in six.iteritems(self._fields):
+            value = getattr(self, slot)
+
+            if field.omit(value):
+                omitted.add((slot, value))
+            else:
+                try:
+                    jobj[field.json_name] = field.encode(value)
+                except errors.SerializationError as error:
+                    raise errors.SerializationError(
+                        'Could not encode {0} ({1}): {2}'.format(
+                            slot, value, error))
+        return jobj
+
+    def to_partial_json(self):
+        return self.fields_to_partial_json()
+
+    @classmethod
+    def _check_required(cls, jobj):
+        missing = set()
+        for _, field in six.iteritems(cls._fields):
+            if not field.omitempty and field.json_name not in jobj:
+                missing.add(field.json_name)
+
+        if missing:
+            raise errors.DeserializationError(
+                'The following fields are required: {0}'.format(
+                    ','.join(missing)))
+
+    @classmethod
+    def fields_from_json(cls, jobj):
+        """Deserialize fields from JSON."""
+        cls._check_required(jobj)
+        fields = {}
+        for slot, field in six.iteritems(cls._fields):
+            if field.json_name not in jobj and field.omitempty:
+                fields[slot] = field.default
+            else:
+                value = jobj[field.json_name]
+                try:
+                    fields[slot] = field.decode(value)
+                except errors.DeserializationError as error:
+                    raise errors.DeserializationError(
+                        'Could not decode {0!r} ({1!r}): {2}'.format(
+                            slot, value, error))
+        return fields
+
+    @classmethod
+    def from_json(cls, jobj):
+        return cls(**cls.fields_from_json(jobj))
+
+
+def encode_b64jose(data):
+    """Encode JOSE Base-64 field.
+
+    :param bytes data:
+    :rtype: `unicode`
+
+    """
+    # b64encode produces ASCII characters only
+    return b64.b64encode(data).decode('ascii')
+
+
+def decode_b64jose(data, size=None, minimum=False):
+    """Decode JOSE Base-64 field.
+
+    :param unicode data:
+    :param int size: Required length (after decoding).
+    :param bool minimum: If ``True``, then `size` will be treated as
+        minimum required length, as opposed to exact equality.
+
+    :rtype: bytes
+
+    """
+    error_cls = TypeError if six.PY2 else binascii.Error
+    try:
+        decoded = b64.b64decode(data.encode())
+    except error_cls as error:
+        raise errors.DeserializationError(error)
+
+    if size is not None and ((not minimum and len(decoded) != size) or
+                             (minimum and len(decoded) < size)):
+        raise errors.DeserializationError(
+            "Expected at least or exactly {0} bytes".format(size))
+
+    return decoded
+
+
+def encode_hex16(value):
+    """Hexlify.
+
+    :param bytes value:
+    :rtype: unicode
+
+    """
+    return binascii.hexlify(value).decode()
+
+
+def decode_hex16(value, size=None, minimum=False):
+    """Decode hexlified field.
+
+    :param unicode value:
+    :param int size: Required length (after decoding).
+    :param bool minimum: If ``True``, then `size` will be treated as
+        minimum required length, as opposed to exact equality.
+
+    :rtype: bytes
+
+    """
+    value = value.encode()
+    if size is not None and ((not minimum and len(value) != size * 2) or
+                             (minimum and len(value) < size * 2)):
+        raise errors.DeserializationError()
+    error_cls = TypeError if six.PY2 else binascii.Error
+    try:
+        return binascii.unhexlify(value)
+    except error_cls as error:
+        raise errors.DeserializationError(error)
+
+
+def encode_cert(cert):
+    """Encode certificate as JOSE Base-64 DER.
+
+    :type cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
+    :rtype: unicode
+
+    """
+    return encode_b64jose(OpenSSL.crypto.dump_certificate(
+        OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped))
+
+
+def decode_cert(b64der):
+    """Decode JOSE Base-64 DER-encoded certificate.
+
+    :param unicode b64der:
+    :rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
+
+    """
+    try:
+        return util.ComparableX509(OpenSSL.crypto.load_certificate(
+            OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der)))
+    except OpenSSL.crypto.Error as error:
+        raise errors.DeserializationError(error)
+
+
+def encode_csr(csr):
+    """Encode CSR as JOSE Base-64 DER.
+
+    :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
+    :rtype: unicode
+
+    """
+    return encode_b64jose(OpenSSL.crypto.dump_certificate_request(
+        OpenSSL.crypto.FILETYPE_ASN1, csr.wrapped))
+
+
+def decode_csr(b64der):
+    """Decode JOSE Base-64 DER-encoded CSR.
+
+    :param unicode b64der:
+    :rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
+
+    """
+    try:
+        return util.ComparableX509(OpenSSL.crypto.load_certificate_request(
+            OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der)))
+    except OpenSSL.crypto.Error as error:
+        raise errors.DeserializationError(error)
+
+
+class TypedJSONObjectWithFields(JSONObjectWithFields):
+    """JSON object with type."""
+
+    typ = NotImplemented
+    """Type of the object. Subclasses must override."""
+
+    type_field_name = "type"
+    """Field name used to distinguish different object types.
+
+    Subclasses will probably have to override this.
+
+    """
+
+    TYPES = NotImplemented
+    """Types registered for JSON deserialization"""
+
+    @classmethod
+    def register(cls, type_cls, typ=None):
+        """Register class for JSON deserialization."""
+        typ = type_cls.typ if typ is None else typ
+        cls.TYPES[typ] = type_cls
+        return type_cls
+
+    @classmethod
+    def get_type_cls(cls, jobj):
+        """Get the registered class for ``jobj``."""
+        if cls in six.itervalues(cls.TYPES):
+            if cls.type_field_name not in jobj:
+                raise errors.DeserializationError(
+                    "Missing type field ({0})".format(cls.type_field_name))
+            # cls is already registered type_cls, force to use it
+            # so that, e.g Revocation.from_json(jobj) fails if
+            # jobj["type"] != "revocation".
+            return cls
+
+        if not isinstance(jobj, dict):
+            raise errors.DeserializationError(
+                "{0} is not a dictionary object".format(jobj))
+        try:
+            typ = jobj[cls.type_field_name]
+        except KeyError:
+            raise errors.DeserializationError("missing type field")
+
+        try:
+            return cls.TYPES[typ]
+        except KeyError:
+            raise errors.UnrecognizedTypeError(typ, jobj)
+
+    def to_partial_json(self):
+        """Get JSON serializable object.
+
+        :returns: Serializable JSON object representing ACME typed object.
+            :meth:`validate` will almost certainly not work, due to reasons
+            explained in :class:`josepy.interfaces.IJSONSerializable`.
+        :rtype: dict
+
+        """
+        jobj = self.fields_to_partial_json()
+        jobj[self.type_field_name] = self.typ
+        return jobj
+
+    @classmethod
+    def from_json(cls, jobj):
+        """Deserialize ACME object from valid JSON object.
+
+        :raises josepy.errors.UnrecognizedTypeError: if type
+            of the ACME object has not been registered.
+
+        """
+        # make sure subclasses don't cause infinite recursive from_json calls
+        type_cls = cls.get_type_cls(jobj)
+        return type_cls(**type_cls.fields_from_json(jobj))

+ 376 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/json_util_test.py

@@ -0,0 +1,376 @@
+"""Tests for josepy.json_util."""
+import itertools
+import unittest
+
+import mock
+import six
+
+from josepy import errors, interfaces, test_util, util
+
+CERT = test_util.load_comparable_cert('cert.pem')
+CSR = test_util.load_comparable_csr('csr.pem')
+
+
+class FieldTest(unittest.TestCase):
+    """Tests for josepy.json_util.Field."""
+
+    def test_no_omit_boolean(self):
+        from josepy.json_util import Field
+        for default, omitempty, value in itertools.product(
+                [True, False], [True, False], [True, False]):
+            self.assertFalse(
+                Field("foo", default=default, omitempty=omitempty).omit(value))
+
+    def test_descriptors(self):
+        mock_value = mock.MagicMock()
+
+        # pylint: disable=missing-docstring
+
+        def decoder(unused_value):
+            return 'd'
+
+        def encoder(unused_value):
+            return 'e'
+
+        from josepy.json_util import Field
+        field = Field('foo')
+
+        field = field.encoder(encoder)
+        self.assertEqual('e', field.encode(mock_value))
+
+        field = field.decoder(decoder)
+        self.assertEqual('e', field.encode(mock_value))
+        self.assertEqual('d', field.decode(mock_value))
+
+    def test_default_encoder_is_partial(self):
+        class MockField(interfaces.JSONDeSerializable):
+            # pylint: disable=missing-docstring
+            def to_partial_json(self):
+                return 'foo'  # pragma: no cover
+
+            @classmethod
+            def from_json(cls, jobj):
+                pass  # pragma: no cover
+        mock_field = MockField()
+
+        from josepy.json_util import Field
+        self.assertTrue(Field.default_encoder(mock_field) is mock_field)
+        # in particular...
+        self.assertNotEqual('foo', Field.default_encoder(mock_field))
+
+    def test_default_encoder_passthrough(self):
+        mock_value = mock.MagicMock()
+        from josepy.json_util import Field
+        self.assertTrue(Field.default_encoder(mock_value) is mock_value)
+
+    def test_default_decoder_list_to_tuple(self):
+        from josepy.json_util import Field
+        self.assertEqual((1, 2, 3), Field.default_decoder([1, 2, 3]))
+
+    def test_default_decoder_dict_to_frozendict(self):
+        from josepy.json_util import Field
+        obj = Field.default_decoder({'x': 2})
+        self.assertTrue(isinstance(obj, util.frozendict))
+        self.assertEqual(obj, util.frozendict(x=2))
+
+    def test_default_decoder_passthrough(self):
+        mock_value = mock.MagicMock()
+        from josepy.json_util import Field
+        self.assertTrue(Field.default_decoder(mock_value) is mock_value)
+
+
+class JSONObjectWithFieldsMetaTest(unittest.TestCase):
+    """Tests for josepy.json_util.JSONObjectWithFieldsMeta."""
+
+    def setUp(self):
+        from josepy.json_util import Field
+        from josepy.json_util import JSONObjectWithFieldsMeta
+        self.field = Field('Baz')
+        self.field2 = Field('Baz2')
+        # pylint: disable=invalid-name,missing-docstring,too-few-public-methods
+        # pylint: disable=blacklisted-name
+
+        @six.add_metaclass(JSONObjectWithFieldsMeta)
+        class A(object):
+            __slots__ = ('bar',)
+            baz = self.field
+
+        class B(A):
+            pass
+
+        class C(A):
+            baz = self.field2
+
+        self.a_cls = A
+        self.b_cls = B
+        self.c_cls = C
+
+    def test_fields(self):
+        # pylint: disable=protected-access,no-member
+        self.assertEqual({'baz': self.field}, self.a_cls._fields)
+        self.assertEqual({'baz': self.field}, self.b_cls._fields)
+
+    def test_fields_inheritance(self):
+        # pylint: disable=protected-access,no-member
+        self.assertEqual({'baz': self.field2}, self.c_cls._fields)
+
+    def test_slots(self):
+        self.assertEqual(('bar', 'baz'), self.a_cls.__slots__)
+        self.assertEqual(('baz',), self.b_cls.__slots__)
+
+    def test_orig_slots(self):
+        # pylint: disable=protected-access,no-member
+        self.assertEqual(('bar',), self.a_cls._orig_slots)
+        self.assertEqual((), self.b_cls._orig_slots)
+
+
+class JSONObjectWithFieldsTest(unittest.TestCase):
+    """Tests for josepy.json_util.JSONObjectWithFields."""
+    # pylint: disable=protected-access
+
+    def setUp(self):
+        from josepy.json_util import JSONObjectWithFields
+        from josepy.json_util import Field
+
+        class MockJSONObjectWithFields(JSONObjectWithFields):
+            # pylint: disable=invalid-name,missing-docstring,no-self-argument
+            # pylint: disable=too-few-public-methods
+            x = Field('x', omitempty=True,
+                      encoder=(lambda x: x * 2),
+                      decoder=(lambda x: x / 2))
+            y = Field('y')
+            z = Field('Z')  # on purpose uppercase
+
+            @y.encoder
+            def y(value):
+                if value == 500:
+                    raise errors.SerializationError()
+                return value
+
+            @y.decoder
+            def y(value):
+                if value == 500:
+                    raise errors.DeserializationError()
+                return value
+
+        # pylint: disable=invalid-name
+        self.MockJSONObjectWithFields = MockJSONObjectWithFields
+        self.mock = MockJSONObjectWithFields(x=None, y=2, z=3)
+
+    def test_init_defaults(self):
+        self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3))
+
+    def test_encode(self):
+        self.assertEqual(10, self.MockJSONObjectWithFields(
+            x=5, y=0, z=0).encode("x"))
+
+    def test_encode_wrong_field(self):
+        self.assertRaises(errors.Error, self.mock.encode, 'foo')
+
+    def test_encode_serialization_error_passthrough(self):
+        self.assertRaises(
+            errors.SerializationError,
+            self.MockJSONObjectWithFields(y=500, z=None).encode, "y")
+
+    def test_fields_to_partial_json_omits_empty(self):
+        self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3})
+
+    def test_fields_from_json_fills_default_for_empty(self):
+        self.assertEqual(
+            {'x': None, 'y': 2, 'z': 3},
+            self.MockJSONObjectWithFields.fields_from_json({'y': 2, 'Z': 3}))
+
+    def test_fields_from_json_fails_on_missing(self):
+        self.assertRaises(
+            errors.DeserializationError,
+            self.MockJSONObjectWithFields.fields_from_json, {'y': 0})
+        self.assertRaises(
+            errors.DeserializationError,
+            self.MockJSONObjectWithFields.fields_from_json, {'Z': 0})
+        self.assertRaises(
+            errors.DeserializationError,
+            self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'y': 0})
+        self.assertRaises(
+            errors.DeserializationError,
+            self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'Z': 0})
+
+    def test_fields_to_partial_json_encoder(self):
+        self.assertEqual(
+            self.MockJSONObjectWithFields(x=1, y=2, z=3).to_partial_json(),
+            {'x': 2, 'y': 2, 'Z': 3})
+
+    def test_fields_from_json_decoder(self):
+        self.assertEqual(
+            {'x': 2, 'y': 2, 'z': 3},
+            self.MockJSONObjectWithFields.fields_from_json(
+                {'x': 4, 'y': 2, 'Z': 3}))
+
+    def test_fields_to_partial_json_error_passthrough(self):
+        self.assertRaises(
+            errors.SerializationError, self.MockJSONObjectWithFields(
+                x=1, y=500, z=3).to_partial_json)
+
+    def test_fields_from_json_error_passthrough(self):
+        self.assertRaises(
+            errors.DeserializationError,
+            self.MockJSONObjectWithFields.from_json,
+            {'x': 4, 'y': 500, 'Z': 3})
+
+
+class DeEncodersTest(unittest.TestCase):
+    def setUp(self):
+        self.b64_cert = (
+            u'MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhM'
+            u'CVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKz'
+            u'ApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxF'
+            u'DASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIx'
+            u'ODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRI'
+            u'wEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTW'
+            u'ljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwD'
+            u'QYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1'
+            u'AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwE'
+            u'AATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMnd'
+            u'fk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o'
+        )
+        self.b64_csr = (
+            u'MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2F'
+            u'uMRIwEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECw'
+            u'wWVW5pdmVyc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb'
+            u'20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD_N_HW9As'
+            u'dRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3'
+            u'C5QIDAQABoCkwJwYJKoZIhvcNAQkOMRowGDAWBgNVHREEDzANggtleGFtcG'
+            u'xlLmNvbTANBgkqhkiG9w0BAQsFAANBAHJH_O6BtC9aGzEVCMGOZ7z9iIRHW'
+            u'Szr9x_bOzn7hLwsbXPAgO1QxEwL-X-4g20Gn9XBE1N9W6HCIEut2d8wACg'
+        )
+
+    def test_encode_b64jose(self):
+        from josepy.json_util import encode_b64jose
+        encoded = encode_b64jose(b'x')
+        self.assertTrue(isinstance(encoded, six.string_types))
+        self.assertEqual(u'eA', encoded)
+
+    def test_decode_b64jose(self):
+        from josepy.json_util import decode_b64jose
+        decoded = decode_b64jose(u'eA')
+        self.assertTrue(isinstance(decoded, six.binary_type))
+        self.assertEqual(b'x', decoded)
+
+    def test_decode_b64jose_padding_error(self):
+        from josepy.json_util import decode_b64jose
+        self.assertRaises(errors.DeserializationError, decode_b64jose, u'x')
+
+    def test_decode_b64jose_size(self):
+        from josepy.json_util import decode_b64jose
+        self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3))
+        self.assertRaises(
+            errors.DeserializationError, decode_b64jose, u'Zm9v', size=2)
+        self.assertRaises(
+            errors.DeserializationError, decode_b64jose, u'Zm9v', size=4)
+
+    def test_decode_b64jose_minimum_size(self):
+        from josepy.json_util import decode_b64jose
+        self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3, minimum=True))
+        self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=2, minimum=True))
+        self.assertRaises(errors.DeserializationError, decode_b64jose,
+                          u'Zm9v', size=4, minimum=True)
+
+    def test_encode_hex16(self):
+        from josepy.json_util import encode_hex16
+        encoded = encode_hex16(b'foo')
+        self.assertEqual(u'666f6f', encoded)
+        self.assertTrue(isinstance(encoded, six.string_types))
+
+    def test_decode_hex16(self):
+        from josepy.json_util import decode_hex16
+        decoded = decode_hex16(u'666f6f')
+        self.assertEqual(b'foo', decoded)
+        self.assertTrue(isinstance(decoded, six.binary_type))
+
+    def test_decode_hex16_minimum_size(self):
+        from josepy.json_util import decode_hex16
+        self.assertEqual(b'foo', decode_hex16(u'666f6f', size=3, minimum=True))
+        self.assertEqual(b'foo', decode_hex16(u'666f6f', size=2, minimum=True))
+        self.assertRaises(errors.DeserializationError, decode_hex16,
+                          u'666f6f', size=4, minimum=True)
+
+    def test_decode_hex16_odd_length(self):
+        from josepy.json_util import decode_hex16
+        self.assertRaises(errors.DeserializationError, decode_hex16, u'x')
+
+    def test_encode_cert(self):
+        from josepy.json_util import encode_cert
+        self.assertEqual(self.b64_cert, encode_cert(CERT))
+
+    def test_decode_cert(self):
+        from josepy.json_util import decode_cert
+        cert = decode_cert(self.b64_cert)
+        self.assertTrue(isinstance(cert, util.ComparableX509))
+        self.assertEqual(cert, CERT)
+        self.assertRaises(errors.DeserializationError, decode_cert, u'')
+
+    def test_encode_csr(self):
+        from josepy.json_util import encode_csr
+        self.assertEqual(self.b64_csr, encode_csr(CSR))
+
+    def test_decode_csr(self):
+        from josepy.json_util import decode_csr
+        csr = decode_csr(self.b64_csr)
+        self.assertTrue(isinstance(csr, util.ComparableX509))
+        self.assertEqual(csr, CSR)
+        self.assertRaises(errors.DeserializationError, decode_csr, u'')
+
+
+class TypedJSONObjectWithFieldsTest(unittest.TestCase):
+
+    def setUp(self):
+        from josepy.json_util import TypedJSONObjectWithFields
+
+        # pylint: disable=missing-docstring,abstract-method
+        # pylint: disable=too-few-public-methods
+
+        class MockParentTypedJSONObjectWithFields(TypedJSONObjectWithFields):
+            TYPES = {}
+            type_field_name = 'type'
+
+        @MockParentTypedJSONObjectWithFields.register
+        class MockTypedJSONObjectWithFields(
+                MockParentTypedJSONObjectWithFields):
+            typ = 'test'
+            __slots__ = ('foo',)
+
+            @classmethod
+            def fields_from_json(cls, jobj):
+                return {'foo': jobj['foo']}
+
+            def fields_to_partial_json(self):
+                return {'foo': self.foo}
+
+        self.parent_cls = MockParentTypedJSONObjectWithFields
+        self.msg = MockTypedJSONObjectWithFields(foo='bar')
+
+    def test_to_partial_json(self):
+        self.assertEqual(self.msg.to_partial_json(), {
+            'type': 'test',
+            'foo': 'bar',
+        })
+
+    def test_from_json_non_dict_fails(self):
+        for value in [[], (), 5, "asd"]:  # all possible input types
+            self.assertRaises(
+                errors.DeserializationError, self.parent_cls.from_json, value)
+
+    def test_from_json_dict_no_type_fails(self):
+        self.assertRaises(
+            errors.DeserializationError, self.parent_cls.from_json, {})
+
+    def test_from_json_unknown_type_fails(self):
+        self.assertRaises(errors.UnrecognizedTypeError,
+                          self.parent_cls.from_json, {'type': 'bar'})
+
+    def test_from_json_returns_obj(self):
+        self.assertEqual({'foo': 'bar'}, self.parent_cls.from_json(
+            {'type': 'test', 'foo': 'bar'}))
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 199 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/jwa.py

@@ -0,0 +1,199 @@
+"""JSON Web Algorithms.
+
+https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
+
+"""
+import abc
+import collections
+import logging
+
+import cryptography.exceptions
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes  # type: ignore
+from cryptography.hazmat.primitives import hmac  # type: ignore
+from cryptography.hazmat.primitives.asymmetric import padding  # type: ignore
+
+from josepy import errors, interfaces, jwk
+
+logger = logging.getLogger(__name__)
+
+
+class JWA(interfaces.JSONDeSerializable):  # pylint: disable=abstract-method
+    # pylint: disable=too-few-public-methods
+    # for some reason disable=abstract-method has to be on the line
+    # above...
+    """JSON Web Algorithm."""
+
+
+class JWASignature(JWA, collections.Hashable):  # type: ignore
+    """Base class for JSON Web Signature Algorithms."""
+    SIGNATURES = {}  # type: dict
+
+    def __init__(self, name):
+        self.name = name
+
+    def __eq__(self, other):
+        if not isinstance(other, JWASignature):
+            return NotImplemented
+        return self.name == other.name
+
+    def __hash__(self):
+        return hash((self.__class__, self.name))
+
+    def __ne__(self, other):
+        return not self == other
+
+    @classmethod
+    def register(cls, signature_cls):
+        """Register class for JSON deserialization."""
+        cls.SIGNATURES[signature_cls.name] = signature_cls
+        return signature_cls
+
+    def to_partial_json(self):
+        return self.name
+
+    @classmethod
+    def from_json(cls, jobj):
+        return cls.SIGNATURES[jobj]
+
+    @abc.abstractmethod
+    def sign(self, key, msg):  # pragma: no cover
+        """Sign the ``msg`` using ``key``."""
+        raise NotImplementedError()
+
+    @abc.abstractmethod
+    def verify(self, key, msg, sig):  # pragma: no cover
+        """Verify the ``msg`` and ``sig`` using ``key``."""
+        raise NotImplementedError()
+
+    def __repr__(self):
+        return self.name
+
+
+class _JWAHS(JWASignature):
+
+    kty = jwk.JWKOct
+
+    def __init__(self, name, hash_):
+        super(_JWAHS, self).__init__(name)
+        self.hash = hash_()
+
+    def sign(self, key, msg):
+        signer = hmac.HMAC(key, self.hash, backend=default_backend())
+        signer.update(msg)
+        return signer.finalize()
+
+    def verify(self, key, msg, sig):
+        verifier = hmac.HMAC(key, self.hash, backend=default_backend())
+        verifier.update(msg)
+        try:
+            verifier.verify(sig)
+        except cryptography.exceptions.InvalidSignature as error:
+            logger.debug(error, exc_info=True)
+            return False
+        else:
+            return True
+
+
+class _JWARSA(object):
+
+    kty = jwk.JWKRSA
+    padding = NotImplemented
+    hash = NotImplemented
+
+    def sign(self, key, msg):
+        """Sign the ``msg`` using ``key``."""
+        # If cryptography library supports new style api (v1.4 and later)
+        new_api = hasattr(key, "sign")
+        try:
+            if new_api:
+                return key.sign(msg, self.padding, self.hash)
+            signer = key.signer(self.padding, self.hash)
+        except AttributeError as error:
+            logger.debug(error, exc_info=True)
+            raise errors.Error("Public key cannot be used for signing")
+        except ValueError as error:  # digest too large
+            logger.debug(error, exc_info=True)
+            raise errors.Error(str(error))
+        signer.update(msg)
+        try:
+            return signer.finalize()
+        except ValueError as error:
+            logger.debug(error, exc_info=True)
+            raise errors.Error(str(error))
+
+    def verify(self, key, msg, sig):
+        """Verify the ``msg` and ``sig`` using ``key``."""
+        # If cryptography library supports new style api (v1.4 and later)
+        new_api = hasattr(key, "verify")
+        if not new_api:
+            verifier = key.verifier(sig, self.padding, self.hash)
+            verifier.update(msg)
+        try:
+            if new_api:
+                key.verify(sig, msg, self.padding, self.hash)
+            else:
+                verifier.verify()
+        except cryptography.exceptions.InvalidSignature as error:
+            logger.debug(error, exc_info=True)
+            return False
+        else:
+            return True
+
+
+class _JWARS(_JWARSA, JWASignature):
+
+    def __init__(self, name, hash_):
+        super(_JWARS, self).__init__(name)
+        self.padding = padding.PKCS1v15()
+        self.hash = hash_()
+
+
+class _JWAPS(_JWARSA, JWASignature):
+
+    def __init__(self, name, hash_):
+        super(_JWAPS, self).__init__(name)
+        self.padding = padding.PSS(
+            mgf=padding.MGF1(hash_()),
+            salt_length=padding.PSS.MAX_LENGTH)
+        self.hash = hash_()
+
+
+class _JWAES(JWASignature):  # pylint: disable=abstract-class-not-used
+
+    # TODO: implement ES signatures
+
+    def sign(self, key, msg):  # pragma: no cover
+        raise NotImplementedError()
+
+    def verify(self, key, msg, sig):  # pragma: no cover
+        raise NotImplementedError()
+
+
+#: HMAC using SHA-256
+HS256 = JWASignature.register(_JWAHS('HS256', hashes.SHA256))
+#: HMAC using SHA-384
+HS384 = JWASignature.register(_JWAHS('HS384', hashes.SHA384))
+#: HMAC using SHA-512
+HS512 = JWASignature.register(_JWAHS('HS512', hashes.SHA512))
+
+#: RSASSA-PKCS-v1_5 using SHA-256
+RS256 = JWASignature.register(_JWARS('RS256', hashes.SHA256))
+#: RSASSA-PKCS-v1_5 using SHA-384
+RS384 = JWASignature.register(_JWARS('RS384', hashes.SHA384))
+#: RSASSA-PKCS-v1_5 using SHA-512
+RS512 = JWASignature.register(_JWARS('RS512', hashes.SHA512))
+
+#: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
+PS256 = JWASignature.register(_JWAPS('PS256', hashes.SHA256))
+#: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
+PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384))
+#: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
+PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512))
+
+#: ECDSA using P-256 and SHA-256
+ES256 = JWASignature.register(_JWAES('ES256'))
+#: ECDSA using P-384 and SHA-384
+ES384 = JWASignature.register(_JWAES('ES384'))
+#: ECDSA using P-521 and SHA-512
+ES512 = JWASignature.register(_JWAES('ES512'))

+ 137 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/jwa_test.py

@@ -0,0 +1,137 @@
+"""Tests for josepy.jwa."""
+import unittest
+
+import mock
+
+from josepy import errors, test_util
+
+RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem')
+RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem')
+RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem')
+
+
+class JWASignatureTest(unittest.TestCase):
+    """Tests for josepy.jwa.JWASignature."""
+
+    def setUp(self):
+        from josepy.jwa import JWASignature
+
+        class MockSig(JWASignature):
+            # pylint: disable=missing-docstring,too-few-public-methods
+            # pylint: disable=abstract-class-not-used
+            def sign(self, key, msg):
+                raise NotImplementedError()  # pragma: no cover
+
+            def verify(self, key, msg, sig):
+                raise NotImplementedError()  # pragma: no cover
+
+        # pylint: disable=invalid-name
+        self.Sig1 = MockSig('Sig1')
+        self.Sig2 = MockSig('Sig2')
+
+    def test_eq(self):
+        self.assertEqual(self.Sig1, self.Sig1)
+
+    def test_ne(self):
+        self.assertNotEqual(self.Sig1, self.Sig2)
+
+    def test_ne_other_type(self):
+        self.assertNotEqual(self.Sig1, 5)
+
+    def test_repr(self):
+        self.assertEqual('Sig1', repr(self.Sig1))
+        self.assertEqual('Sig2', repr(self.Sig2))
+
+    def test_to_partial_json(self):
+        self.assertEqual(self.Sig1.to_partial_json(), 'Sig1')
+        self.assertEqual(self.Sig2.to_partial_json(), 'Sig2')
+
+    def test_from_json(self):
+        from josepy.jwa import JWASignature
+        from josepy.jwa import RS256
+        self.assertTrue(JWASignature.from_json('RS256') is RS256)
+
+
+class JWAHSTest(unittest.TestCase):  # pylint: disable=too-few-public-methods
+
+    def test_it(self):
+        from josepy.jwa import HS256
+        sig = (
+            b"\xceR\xea\xcd\x94\xab\xcf\xfb\xe0\xacA.:\x1a'\x08i\xe2\xc4"
+            b"\r\x85+\x0e\x85\xaeUZ\xd4\xb3\x97zO"
+        )
+        self.assertEqual(HS256.sign(b'some key', b'foo'), sig)
+        self.assertTrue(HS256.verify(b'some key', b'foo', sig) is True)
+        self.assertTrue(HS256.verify(b'some key', b'foo', sig + b'!') is False)
+
+
+class JWARSTest(unittest.TestCase):
+
+    def test_sign_no_private_part(self):
+        from josepy.jwa import RS256
+        self.assertRaises(
+            errors.Error, RS256.sign, RSA512_KEY.public_key(), b'foo')
+
+    def test_sign_key_too_small(self):
+        from josepy.jwa import RS256
+        from josepy.jwa import PS256
+        self.assertRaises(errors.Error, RS256.sign, RSA256_KEY, b'foo')
+        self.assertRaises(errors.Error, PS256.sign, RSA256_KEY, b'foo')
+
+    def test_rs(self):
+        from josepy.jwa import RS256
+        sig = (
+            b'|\xc6\xb2\xa4\xab(\x87\x99\xfa*:\xea\xf8\xa0N&}\x9f\x0f\xc0O'
+            b'\xc6t\xa3\xe6\xfa\xbb"\x15Y\x80Y\xe0\x81\xb8\x88)\xba\x0c\x9c'
+            b'\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99'
+            b'\xd2\xb9.>}\xfd'
+        )
+        self.assertEqual(RS256.sign(RSA512_KEY, b'foo'), sig)
+        self.assertTrue(RS256.verify(RSA512_KEY.public_key(), b'foo', sig))
+        self.assertFalse(RS256.verify(
+            RSA512_KEY.public_key(), b'foo', sig + b'!'))
+
+    def test_ps(self):
+        from josepy.jwa import PS256
+        sig = PS256.sign(RSA1024_KEY, b'foo')
+        self.assertTrue(PS256.verify(RSA1024_KEY.public_key(), b'foo', sig))
+        self.assertFalse(PS256.verify(
+            RSA1024_KEY.public_key(), b'foo', sig + b'!'))
+
+    def test_sign_new_api(self):
+        from josepy.jwa import RS256
+        key = mock.MagicMock()
+        RS256.sign(key, "message")
+        self.assertTrue(key.sign.called)
+
+    def test_sign_old_api(self):
+        from josepy.jwa import RS256
+        key = mock.MagicMock(spec=[u'signer'])
+        signer = mock.MagicMock()
+        key.signer.return_value = signer
+        RS256.sign(key, "message")
+        self.assertTrue(all([
+            key.signer.called,
+            signer.update.called,
+            signer.finalize.called]))
+
+    def test_verify_new_api(self):
+        from josepy.jwa import RS256
+        key = mock.MagicMock()
+        RS256.verify(key, "message", "signature")
+        self.assertTrue(key.verify.called)
+
+    def test_verify_old_api(self):
+        from josepy.jwa import RS256
+        key = mock.MagicMock(spec=[u'verifier'])
+        verifier = mock.MagicMock()
+        key.verifier.return_value = verifier
+        RS256.verify(key, "message", "signature")
+        self.assertTrue(all([
+            key.verifier.called,
+            verifier.update.called,
+            verifier.verify.called]))
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 277 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/jwk.py

@@ -0,0 +1,277 @@
+"""JSON Web Key."""
+import abc
+import binascii
+import json
+import logging
+
+import cryptography.exceptions
+import six
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes  # type: ignore
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ec  # type: ignore
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+from josepy import errors, json_util, util
+
+logger = logging.getLogger(__name__)
+
+
+class JWK(json_util.TypedJSONObjectWithFields):
+    # pylint: disable=too-few-public-methods
+    """JSON Web Key."""
+    type_field_name = 'kty'
+    TYPES = {}  # type: dict
+    cryptography_key_types = ()  # type: tuple
+    """Subclasses should override."""
+
+    required = NotImplemented
+    """Required members of public key's representation as defined by JWK/JWA."""
+
+    _thumbprint_json_dumps_params = {
+        # "no whitespace or line breaks before or after any syntactic
+        # elements"
+        'indent': None,
+        'separators': (',', ':'),
+        # "members ordered lexicographically by the Unicode [UNICODE]
+        # code points of the member names"
+        'sort_keys': True,
+    }
+
+    def thumbprint(self, hash_function=hashes.SHA256):
+        """Compute JWK Thumbprint.
+
+        https://tools.ietf.org/html/rfc7638
+
+        :returns: bytes
+
+        """
+        digest = hashes.Hash(hash_function(), backend=default_backend())
+        digest.update(json.dumps(
+            dict((k, v) for k, v in six.iteritems(self.to_json())
+                 if k in self.required),
+            **self._thumbprint_json_dumps_params).encode())
+        return digest.finalize()
+
+    @abc.abstractmethod
+    def public_key(self):  # pragma: no cover
+        """Generate JWK with public key.
+
+        For symmetric cryptosystems, this would return ``self``.
+
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def _load_cryptography_key(cls, data, password=None, backend=None):
+        backend = default_backend() if backend is None else backend
+        exceptions = {}
+
+        # private key?
+        for loader in (serialization.load_pem_private_key,
+                       serialization.load_der_private_key):
+            try:
+                return loader(data, password, backend)
+            except (ValueError, TypeError,
+                    cryptography.exceptions.UnsupportedAlgorithm) as error:
+                exceptions[loader] = error
+
+        # public key?
+        for loader in (serialization.load_pem_public_key,
+                       serialization.load_der_public_key):
+            try:
+                return loader(data, backend)
+            except (ValueError,
+                    cryptography.exceptions.UnsupportedAlgorithm) as error:
+                exceptions[loader] = error
+
+        # no luck
+        raise errors.Error('Unable to deserialize key: {0}'.format(exceptions))
+
+    @classmethod
+    def load(cls, data, password=None, backend=None):
+        """Load serialized key as JWK.
+
+        :param str data: Public or private key serialized as PEM or DER.
+        :param str password: Optional password.
+        :param backend: A `.PEMSerializationBackend` and
+            `.DERSerializationBackend` provider.
+
+        :raises errors.Error: if unable to deserialize, or unsupported
+            JWK algorithm
+
+        :returns: JWK of an appropriate type.
+        :rtype: `JWK`
+
+        """
+        try:
+            key = cls._load_cryptography_key(data, password, backend)
+        except errors.Error as error:
+            logger.debug('Loading symmetric key, asymmetric failed: %s', error)
+            return JWKOct(key=data)
+
+        if cls.typ is not NotImplemented and not isinstance(
+                key, cls.cryptography_key_types):
+            raise errors.Error('Unable to deserialize {0} into {1}'.format(
+                key.__class__, cls.__class__))
+        for jwk_cls in six.itervalues(cls.TYPES):
+            if isinstance(key, jwk_cls.cryptography_key_types):
+                return jwk_cls(key=key)
+        raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__))
+
+
+@JWK.register
+class JWKES(JWK):  # pragma: no cover
+    # pylint: disable=abstract-class-not-used
+    """ES JWK.
+
+    .. warning:: This is not yet implemented!
+
+    """
+    typ = 'ES'
+    cryptography_key_types = (
+        ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)
+    required = ('crv', JWK.type_field_name, 'x', 'y')
+
+    def fields_to_partial_json(self):
+        raise NotImplementedError()
+
+    @classmethod
+    def fields_from_json(cls, jobj):
+        raise NotImplementedError()
+
+    def public_key(self):
+        raise NotImplementedError()
+
+
+@JWK.register
+class JWKOct(JWK):
+    """Symmetric JWK."""
+    typ = 'oct'
+    __slots__ = ('key',)
+    required = ('k', JWK.type_field_name)
+
+    def fields_to_partial_json(self):
+        # TODO: An "alg" member SHOULD also be present to identify the
+        # algorithm intended to be used with the key, unless the
+        # application uses another means or convention to determine
+        # the algorithm used.
+        return {'k': json_util.encode_b64jose(self.key)}
+
+    @classmethod
+    def fields_from_json(cls, jobj):
+        return cls(key=json_util.decode_b64jose(jobj['k']))
+
+    def public_key(self):
+        return self
+
+
+@JWK.register
+class JWKRSA(JWK):
+    """RSA JWK.
+
+    :ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
+        or :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey` wrapped
+        in :class:`~josepy.util.ComparableRSAKey`
+
+    """
+    typ = 'RSA'
+    cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey)
+    __slots__ = ('key',)
+    required = ('e', JWK.type_field_name, 'n')
+
+    def __init__(self, *args, **kwargs):
+        if 'key' in kwargs and not isinstance(
+                kwargs['key'], util.ComparableRSAKey):
+            kwargs['key'] = util.ComparableRSAKey(kwargs['key'])
+        super(JWKRSA, self).__init__(*args, **kwargs)
+
+    @classmethod
+    def _encode_param(cls, data):
+        """Encode Base64urlUInt.
+
+        :type data: long
+        :rtype: unicode
+
+        """
+        def _leading_zeros(arg):
+            if len(arg) % 2:
+                return '0' + arg
+            return arg
+
+        return json_util.encode_b64jose(binascii.unhexlify(
+            _leading_zeros(hex(data)[2:].rstrip('L'))))
+
+    @classmethod
+    def _decode_param(cls, data):
+        """Decode Base64urlUInt."""
+        try:
+            return int(binascii.hexlify(json_util.decode_b64jose(data)), 16)
+        except ValueError:  # invalid literal for long() with base 16
+            raise errors.DeserializationError()
+
+    def public_key(self):
+        return type(self)(key=self.key.public_key())
+
+    @classmethod
+    def fields_from_json(cls, jobj):
+        # pylint: disable=invalid-name
+        n, e = (cls._decode_param(jobj[x]) for x in ('n', 'e'))
+        public_numbers = rsa.RSAPublicNumbers(e=e, n=n)
+        if 'd' not in jobj:  # public key
+            key = public_numbers.public_key(default_backend())
+        else:  # private key
+            d = cls._decode_param(jobj['d'])
+            if ('p' in jobj or 'q' in jobj or 'dp' in jobj or
+                    'dq' in jobj or 'qi' in jobj or 'oth' in jobj):
+                # "If the producer includes any of the other private
+                # key parameters, then all of the others MUST be
+                # present, with the exception of "oth", which MUST
+                # only be present when more than two prime factors
+                # were used."
+                p, q, dp, dq, qi, = all_params = tuple(
+                    jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi'))
+                if tuple(param for param in all_params if param is None):
+                    raise errors.Error(
+                        'Some private parameters are missing: {0}'.format(
+                            all_params))
+                p, q, dp, dq, qi = tuple(
+                    cls._decode_param(x) for x in all_params)
+
+                # TODO: check for oth
+            else:
+                # cryptography>=0.8
+                p, q = rsa.rsa_recover_prime_factors(n, e, d)
+                dp = rsa.rsa_crt_dmp1(d, p)
+                dq = rsa.rsa_crt_dmq1(d, q)
+                qi = rsa.rsa_crt_iqmp(p, q)
+
+            key = rsa.RSAPrivateNumbers(
+                p, q, d, dp, dq, qi, public_numbers).private_key(
+                    default_backend())
+
+        return cls(key=key)
+
+    def fields_to_partial_json(self):
+        # pylint: disable=protected-access
+        if isinstance(self.key._wrapped, rsa.RSAPublicKey):
+            numbers = self.key.public_numbers()
+            params = {
+                'n': numbers.n,
+                'e': numbers.e,
+            }
+        else:  # rsa.RSAPrivateKey
+            private = self.key.private_numbers()
+            public = self.key.public_key().public_numbers()
+            params = {
+                'n': public.n,
+                'e': public.e,
+                'd': private.d,
+                'p': private.p,
+                'q': private.q,
+                'dp': private.dmp1,
+                'dq': private.dmq1,
+                'qi': private.iqmp,
+            }
+        return dict((key, self._encode_param(value))
+                    for key, value in six.iteritems(params))

+ 186 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/jwk_test.py

@@ -0,0 +1,186 @@
+"""Tests for josepy.jwk."""
+import binascii
+import unittest
+
+from josepy import errors, json_util, test_util, util
+
+DSA_PEM = test_util.load_vector('dsa512_key.pem')
+RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem')
+RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem')
+
+
+class JWKTest(unittest.TestCase):
+    """Tests for josepy.jwk.JWK."""
+
+    def test_load(self):
+        from josepy.jwk import JWK
+        self.assertRaises(errors.Error, JWK.load, DSA_PEM)
+
+    def test_load_subclass_wrong_type(self):
+        from josepy.jwk import JWKRSA
+        self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM)
+
+
+class JWKTestBaseMixin(object):
+    """Mixin test for JWK subclass tests."""
+
+    thumbprint = NotImplemented
+
+    def test_thumbprint_private(self):
+        self.assertEqual(self.thumbprint, self.jwk.thumbprint())
+
+    def test_thumbprint_public(self):
+        self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint())
+
+
+class JWKOctTest(unittest.TestCase, JWKTestBaseMixin):
+    """Tests for josepy.jwk.JWKOct."""
+
+    thumbprint = (b"\xf3\xe7\xbe\xa8`\xd2\xdap\xe9}\x9c\xce>"
+                  b"\xd0\xfcI\xbe\xcd\x92'\xd4o\x0e\xf41\xea"
+                  b"\x8e(\x8a\xb2i\x1c")
+
+    def setUp(self):
+        from josepy.jwk import JWKOct
+        self.jwk = JWKOct(key=b'foo')
+        self.jobj = {'kty': 'oct', 'k': json_util.encode_b64jose(b'foo')}
+
+    def test_to_partial_json(self):
+        self.assertEqual(self.jwk.to_partial_json(), self.jobj)
+
+    def test_from_json(self):
+        from josepy.jwk import JWKOct
+        self.assertEqual(self.jwk, JWKOct.from_json(self.jobj))
+
+    def test_from_json_hashable(self):
+        from josepy.jwk import JWKOct
+        hash(JWKOct.from_json(self.jobj))
+
+    def test_load(self):
+        from josepy.jwk import JWKOct
+        self.assertEqual(self.jwk, JWKOct.load(b'foo'))
+
+    def test_public_key(self):
+        self.assertTrue(self.jwk.public_key() is self.jwk)
+
+
+class JWKRSATest(unittest.TestCase, JWKTestBaseMixin):
+    """Tests for josepy.jwk.JWKRSA."""
+    # pylint: disable=too-many-instance-attributes
+
+    thumbprint = (b'\x83K\xdc#3\x98\xca\x98\xed\xcb\x80\x80<\x0c'
+                  b'\xf0\x95\xb9H\xb2*l\xbd$\xe5&|O\x91\xd4 \xb0Y')
+
+    def setUp(self):
+        from josepy.jwk import JWKRSA
+        self.jwk256 = JWKRSA(key=RSA256_KEY.public_key())
+        self.jwk256json = {
+            'kty': 'RSA',
+            'e': 'AQAB',
+            'n': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk',
+        }
+        # pylint: disable=protected-access
+        self.jwk256_not_comparable = JWKRSA(
+            key=RSA256_KEY.public_key()._wrapped)
+        self.jwk512 = JWKRSA(key=RSA512_KEY.public_key())
+        self.jwk512json = {
+            'kty': 'RSA',
+            'e': 'AQAB',
+            'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5'
+                 '80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q',
+        }
+        self.private = JWKRSA(key=RSA256_KEY)
+        self.private_json_small = self.jwk256json.copy()
+        self.private_json_small['d'] = (
+            'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE')
+        self.private_json = self.jwk256json.copy()
+        self.private_json.update({
+            'd': 'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE',
+            'p': 'zUVNZn4lLLBD1R6NE8TKNQ',
+            'q': 'wcfKfc7kl5jfqXArCRSURQ',
+            'dp': 'CWJFq43QvT5Bm5iN8n1okQ',
+            'dq': 'bHh2u7etM8LKKCF2pY2UdQ',
+            'qi': 'oi45cEkbVoJjAbnQpFY87Q',
+        })
+        self.jwk = self.private
+
+    def test_init_auto_comparable(self):
+        self.assertTrue(isinstance(
+            self.jwk256_not_comparable.key, util.ComparableRSAKey))
+        self.assertEqual(self.jwk256, self.jwk256_not_comparable)
+
+    def test_encode_param_zero(self):
+        from josepy.jwk import JWKRSA
+        # pylint: disable=protected-access
+        # TODO: move encode/decode _param to separate class
+        self.assertEqual('AA', JWKRSA._encode_param(0))
+
+    def test_equals(self):
+        self.assertEqual(self.jwk256, self.jwk256)
+        self.assertEqual(self.jwk512, self.jwk512)
+
+    def test_not_equals(self):
+        self.assertNotEqual(self.jwk256, self.jwk512)
+        self.assertNotEqual(self.jwk512, self.jwk256)
+
+    def test_load(self):
+        from josepy.jwk import JWKRSA
+        self.assertEqual(self.private, JWKRSA.load(
+            test_util.load_vector('rsa256_key.pem')))
+
+    def test_public_key(self):
+        self.assertEqual(self.jwk256, self.private.public_key())
+
+    def test_to_partial_json(self):
+        self.assertEqual(self.jwk256.to_partial_json(), self.jwk256json)
+        self.assertEqual(self.jwk512.to_partial_json(), self.jwk512json)
+        self.assertEqual(self.private.to_partial_json(), self.private_json)
+
+    def test_from_json(self):
+        from josepy.jwk import JWK
+        self.assertEqual(
+            self.jwk256, JWK.from_json(self.jwk256json))
+        self.assertEqual(
+            self.jwk512, JWK.from_json(self.jwk512json))
+        self.assertEqual(self.private, JWK.from_json(self.private_json))
+
+    def test_from_json_private_small(self):
+        from josepy.jwk import JWK
+        self.assertEqual(self.private, JWK.from_json(self.private_json_small))
+
+    def test_from_json_missing_one_additional(self):
+        from josepy.jwk import JWK
+        del self.private_json['q']
+        self.assertRaises(errors.Error, JWK.from_json, self.private_json)
+
+    def test_from_json_hashable(self):
+        from josepy.jwk import JWK
+        hash(JWK.from_json(self.jwk256json))
+
+    def test_from_json_non_schema_errors(self):
+        # valid against schema, but still failing
+        from josepy.jwk import JWK
+        self.assertRaises(errors.DeserializationError, JWK.from_json,
+                          {'kty': 'RSA', 'e': 'AQAB', 'n': ''})
+        self.assertRaises(errors.DeserializationError, JWK.from_json,
+                          {'kty': 'RSA', 'e': 'AQAB', 'n': '1'})
+
+    def test_thumbprint_go_jose(self):
+        # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk.go#L155
+        # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L331-L344
+        # https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L384
+        from josepy.jwk import JWKRSA
+        key = JWKRSA.json_loads("""{
+    "kty": "RSA",
+    "kid": "bilbo.baggins@hobbiton.example",
+    "use": "sig",
+    "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw",
+    "e": "AQAB"
+}""")
+        self.assertEqual(
+            binascii.hexlify(key.thumbprint()),
+            b"f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932")
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 430 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/jws.py

@@ -0,0 +1,430 @@
+"""JSON Web Signature."""
+import argparse
+import base64
+import sys
+
+import OpenSSL
+import six
+
+from josepy import b64, errors, json_util, jwa, jwk, util
+
+
+class MediaType(object):
+    """MediaType field encoder/decoder."""
+
+    PREFIX = 'application/'
+    """MIME Media Type and Content Type prefix."""
+
+    @classmethod
+    def decode(cls, value):
+        """Decoder."""
+        # 4.1.10
+        if '/' not in value:
+            if ';' in value:
+                raise errors.DeserializationError('Unexpected semi-colon')
+            return cls.PREFIX + value
+        return value
+
+    @classmethod
+    def encode(cls, value):
+        """Encoder."""
+        # 4.1.10
+        if ';' not in value:
+            assert value.startswith(cls.PREFIX)
+            return value[len(cls.PREFIX):]
+        return value
+
+
+class Header(json_util.JSONObjectWithFields):
+    """JOSE Header.
+
+    .. warning:: This class supports **only** Registered Header
+        Parameter Names (as defined in section 4.1 of the
+        protocol). If you need Public Header Parameter Names (4.2)
+        or Private Header Parameter Names (4.3), you must subclass
+        and override :meth:`from_json` and :meth:`to_partial_json`
+        appropriately.
+
+    .. warning:: This class does not support any extensions through
+        the "crit" (Critical) Header Parameter (4.1.11) and as a
+        conforming implementation, :meth:`from_json` treats its
+        occurrence as an error. Please subclass if you seek for
+        a different behaviour.
+
+    :ivar x5tS256: "x5t#S256"
+    :ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`.
+    :ivar str cty: Content-Type, inc. :const:`MediaType.PREFIX`.
+
+    """
+    alg = json_util.Field(
+        'alg', decoder=jwa.JWASignature.from_json, omitempty=True)
+    jku = json_util.Field('jku', omitempty=True)
+    jwk = json_util.Field('jwk', decoder=jwk.JWK.from_json, omitempty=True)
+    kid = json_util.Field('kid', omitempty=True)
+    x5u = json_util.Field('x5u', omitempty=True)
+    x5c = json_util.Field('x5c', omitempty=True, default=())
+    x5t = json_util.Field(
+        'x5t', decoder=json_util.decode_b64jose, omitempty=True)
+    x5tS256 = json_util.Field(
+        'x5t#S256', decoder=json_util.decode_b64jose, omitempty=True)
+    typ = json_util.Field('typ', encoder=MediaType.encode,
+                          decoder=MediaType.decode, omitempty=True)
+    cty = json_util.Field('cty', encoder=MediaType.encode,
+                          decoder=MediaType.decode, omitempty=True)
+    crit = json_util.Field('crit', omitempty=True, default=())
+
+    def not_omitted(self):
+        """Fields that would not be omitted in the JSON object."""
+        return dict((name, getattr(self, name))
+                    for name, field in six.iteritems(self._fields)
+                    if not field.omit(getattr(self, name)))
+
+    def __add__(self, other):
+        if not isinstance(other, type(self)):
+            raise TypeError('Header cannot be added to: {0}'.format(
+                type(other)))
+
+        not_omitted_self = self.not_omitted()
+        not_omitted_other = other.not_omitted()
+
+        if set(not_omitted_self).intersection(not_omitted_other):
+            raise TypeError('Addition of overlapping headers not defined')
+
+        not_omitted_self.update(not_omitted_other)
+        return type(self)(**not_omitted_self)  # pylint: disable=star-args
+
+    def find_key(self):
+        """Find key based on header.
+
+        .. todo:: Supports only "jwk" header parameter lookup.
+
+        :returns: (Public) key found in the header.
+        :rtype: .JWK
+
+        :raises josepy.errors.Error: if key could not be found
+
+        """
+        if self.jwk is None:
+            raise errors.Error('No key found')
+        return self.jwk
+
+    @crit.decoder
+    def crit(unused_value):
+        # pylint: disable=missing-docstring,no-self-argument,no-self-use
+        raise errors.DeserializationError(
+            '"crit" is not supported, please subclass')
+
+    # x5c does NOT use JOSE Base64 (4.1.6)
+
+    @x5c.encoder  # type: ignore
+    def x5c(value):  # pylint: disable=missing-docstring,no-self-argument
+        return [base64.b64encode(OpenSSL.crypto.dump_certificate(
+            OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) for cert in value]
+
+    @x5c.decoder  # type: ignore
+    def x5c(value):  # pylint: disable=missing-docstring,no-self-argument
+        try:
+            return tuple(util.ComparableX509(OpenSSL.crypto.load_certificate(
+                OpenSSL.crypto.FILETYPE_ASN1,
+                base64.b64decode(cert))) for cert in value)
+        except OpenSSL.crypto.Error as error:
+            raise errors.DeserializationError(error)
+
+
+class Signature(json_util.JSONObjectWithFields):
+    """JWS Signature.
+
+    :ivar combined: Combined Header (protected and unprotected,
+        :class:`Header`).
+    :ivar unicode protected: JWS protected header (Jose Base-64 decoded).
+    :ivar header: JWS Unprotected Header (:class:`Header`).
+    :ivar str signature: The signature.
+
+    """
+    header_cls = Header
+
+    __slots__ = ('combined',)
+    protected = json_util.Field('protected', omitempty=True, default='')
+    header = json_util.Field(
+        'header', omitempty=True, default=header_cls(),
+        decoder=header_cls.from_json)
+    signature = json_util.Field(
+        'signature', decoder=json_util.decode_b64jose,
+        encoder=json_util.encode_b64jose)
+
+    @protected.encoder  # type: ignore
+    def protected(value):  # pylint: disable=missing-docstring,no-self-argument
+        # wrong type guess (Signature, not bytes) | pylint: disable=no-member
+        return json_util.encode_b64jose(value.encode('utf-8'))
+
+    @protected.decoder  # type: ignore
+    def protected(value):  # pylint: disable=missing-docstring,no-self-argument
+        return json_util.decode_b64jose(value).decode('utf-8')
+
+    def __init__(self, **kwargs):
+        if 'combined' not in kwargs:
+            kwargs = self._with_combined(kwargs)
+        super(Signature, self).__init__(**kwargs)
+        assert self.combined.alg is not None
+
+    @classmethod
+    def _with_combined(cls, kwargs):
+        assert 'combined' not in kwargs
+        header = kwargs.get('header', cls._fields['header'].default)
+        protected = kwargs.get('protected', cls._fields['protected'].default)
+
+        if protected:
+            combined = header + cls.header_cls.json_loads(protected)
+        else:
+            combined = header
+
+        kwargs['combined'] = combined
+        return kwargs
+
+    @classmethod
+    def _msg(cls, protected, payload):
+        return (b64.b64encode(protected.encode('utf-8')) + b'.' +
+                b64.b64encode(payload))
+
+    def verify(self, payload, key=None):
+        """Verify.
+
+        :param JWK key: Key used for verification.
+
+        """
+        key = self.combined.find_key() if key is None else key
+        return self.combined.alg.verify(
+            key=key.key, sig=self.signature,
+            msg=self._msg(self.protected, payload))
+
+    @classmethod
+    def sign(cls, payload, key, alg, include_jwk=True,
+             protect=frozenset(), **kwargs):
+        """Sign.
+
+        :param JWK key: Key for signature.
+
+        """
+        assert isinstance(key, alg.kty)
+
+        header_params = kwargs
+        header_params['alg'] = alg
+        if include_jwk:
+            header_params['jwk'] = key.public_key()
+
+        assert set(header_params).issubset(cls.header_cls._fields)
+        assert protect.issubset(cls.header_cls._fields)
+
+        protected_params = {}
+        for header in protect:
+            if header in header_params:
+                protected_params[header] = header_params.pop(header)
+        if protected_params:
+            # pylint: disable=star-args
+            protected = cls.header_cls(**protected_params).json_dumps()
+        else:
+            protected = ''
+
+        header = cls.header_cls(**header_params)  # pylint: disable=star-args
+        signature = alg.sign(key.key, cls._msg(protected, payload))
+
+        return cls(protected=protected, header=header, signature=signature)
+
+    def fields_to_partial_json(self):
+        fields = super(Signature, self).fields_to_partial_json()
+        if not fields['header'].not_omitted():
+            del fields['header']
+        return fields
+
+    @classmethod
+    def fields_from_json(cls, jobj):
+        fields = super(Signature, cls).fields_from_json(jobj)
+        fields_with_combined = cls._with_combined(fields)
+        if 'alg' not in fields_with_combined['combined'].not_omitted():
+            raise errors.DeserializationError('alg not present')
+        return fields_with_combined
+
+
+class JWS(json_util.JSONObjectWithFields):
+    """JSON Web Signature.
+
+    :ivar str payload: JWS Payload.
+    :ivar str signature: JWS Signatures.
+
+    """
+    __slots__ = ('payload', 'signatures')
+
+    signature_cls = Signature
+
+    def verify(self, key=None):
+        """Verify."""
+        return all(sig.verify(self.payload, key) for sig in self.signatures)
+
+    @classmethod
+    def sign(cls, payload, **kwargs):
+        """Sign."""
+        return cls(payload=payload, signatures=(
+            cls.signature_cls.sign(payload=payload, **kwargs),))
+
+    @property
+    def signature(self):
+        """Get a singleton signature.
+
+        :rtype: :class:`JWS.signature_cls`
+
+        """
+        assert len(self.signatures) == 1
+        return self.signatures[0]
+
+    def to_compact(self):
+        """Compact serialization.
+
+        :rtype: bytes
+
+        """
+        assert len(self.signatures) == 1
+
+        assert 'alg' not in self.signature.header.not_omitted()
+        # ... it must be in protected
+
+        return (
+            b64.b64encode(self.signature.protected.encode('utf-8')) +
+            b'.' +
+            b64.b64encode(self.payload) +
+            b'.' +
+            b64.b64encode(self.signature.signature))
+
+    @classmethod
+    def from_compact(cls, compact):
+        """Compact deserialization.
+
+        :param bytes compact:
+
+        """
+        try:
+            protected, payload, signature = compact.split(b'.')
+        except ValueError:
+            raise errors.DeserializationError(
+                'Compact JWS serialization should comprise of exactly'
+                ' 3 dot-separated components')
+
+        sig = cls.signature_cls(
+            protected=b64.b64decode(protected).decode('utf-8'),
+            signature=b64.b64decode(signature))
+        return cls(payload=b64.b64decode(payload), signatures=(sig,))
+
+    def to_partial_json(self, flat=True):  # pylint: disable=arguments-differ
+        assert self.signatures
+        payload = json_util.encode_b64jose(self.payload)
+
+        if flat and len(self.signatures) == 1:
+            ret = self.signatures[0].to_partial_json()
+            ret['payload'] = payload
+            return ret
+        else:
+            return {
+                'payload': payload,
+                'signatures': self.signatures,
+            }
+
+    @classmethod
+    def from_json(cls, jobj):
+        if 'signature' in jobj and 'signatures' in jobj:
+            raise errors.DeserializationError('Flat mixed with non-flat')
+        elif 'signature' in jobj:  # flat
+            return cls(payload=json_util.decode_b64jose(jobj.pop('payload')),
+                       signatures=(cls.signature_cls.from_json(jobj),))
+        else:
+            return cls(payload=json_util.decode_b64jose(jobj['payload']),
+                       signatures=tuple(cls.signature_cls.from_json(sig)
+                                        for sig in jobj['signatures']))
+
+
+class CLI(object):
+    """JWS CLI."""
+
+    @classmethod
+    def sign(cls, args):
+        """Sign."""
+        key = args.alg.kty.load(args.key.read())
+        args.key.close()
+        if args.protect is None:
+            args.protect = []
+        if args.compact:
+            args.protect.append('alg')
+
+        sig = JWS.sign(payload=sys.stdin.read().encode(), key=key, alg=args.alg,
+                       protect=set(args.protect))
+
+        if args.compact:
+            six.print_(sig.to_compact().decode('utf-8'))
+        else:  # JSON
+            six.print_(sig.json_dumps_pretty())
+
+    @classmethod
+    def verify(cls, args):
+        """Verify."""
+        if args.compact:
+            sig = JWS.from_compact(sys.stdin.read().encode())
+        else:  # JSON
+            try:
+                sig = JWS.json_loads(sys.stdin.read())
+            except errors.Error as error:
+                six.print_(error)
+                return -1
+
+        if args.key is not None:
+            assert args.kty is not None
+            key = args.kty.load(args.key.read()).public_key()
+            args.key.close()
+        else:
+            key = None
+
+        sys.stdout.write(sig.payload)
+        return not sig.verify(key=key)
+
+    @classmethod
+    def _alg_type(cls, arg):
+        return jwa.JWASignature.from_json(arg)
+
+    @classmethod
+    def _header_type(cls, arg):
+        assert arg in Signature.header_cls._fields
+        return arg
+
+    @classmethod
+    def _kty_type(cls, arg):
+        assert arg in jwk.JWK.TYPES
+        return jwk.JWK.TYPES[arg]
+
+    @classmethod
+    def run(cls, args=None):
+        """Parse arguments and sign/verify."""
+        if args is None:
+            args = sys.argv[1:]
+        parser = argparse.ArgumentParser()
+        parser.add_argument('--compact', action='store_true')
+
+        subparsers = parser.add_subparsers()
+        parser_sign = subparsers.add_parser('sign')
+        parser_sign.set_defaults(func=cls.sign)
+        parser_sign.add_argument(
+            '-k', '--key', type=argparse.FileType('rb'), required=True)
+        parser_sign.add_argument(
+            '-a', '--alg', type=cls._alg_type, default=jwa.RS256)
+        parser_sign.add_argument(
+            '-p', '--protect', action='append', type=cls._header_type)
+
+        parser_verify = subparsers.add_parser('verify')
+        parser_verify.set_defaults(func=cls.verify)
+        parser_verify.add_argument(
+            '-k', '--key', type=argparse.FileType('rb'), required=False)
+        parser_verify.add_argument(
+            '--kty', type=cls._kty_type, required=False)
+
+        parsed = parser.parse_args(args)
+        return parsed.func(parsed)
+
+
+if __name__ == '__main__':
+    exit(CLI.run())  # pragma: no cover

+ 233 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/jws_test.py

@@ -0,0 +1,233 @@
+"""Tests for josepy.jws."""
+import base64
+import unittest
+
+import mock
+import OpenSSL
+
+from josepy import errors, json_util, jwa, jwk, test_util
+
+CERT = test_util.load_comparable_cert('cert.pem')
+KEY = jwk.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
+
+
+class MediaTypeTest(unittest.TestCase):
+    """Tests for josepy.jws.MediaType."""
+
+    def test_decode(self):
+        from josepy.jws import MediaType
+        self.assertEqual('application/app', MediaType.decode('application/app'))
+        self.assertEqual('application/app', MediaType.decode('app'))
+        self.assertRaises(
+            errors.DeserializationError, MediaType.decode, 'app;foo')
+
+    def test_encode(self):
+        from josepy.jws import MediaType
+        self.assertEqual('app', MediaType.encode('application/app'))
+        self.assertEqual('application/app;foo',
+                         MediaType.encode('application/app;foo'))
+
+
+class HeaderTest(unittest.TestCase):
+    """Tests for josepy.jws.Header."""
+
+    def setUp(self):
+        from josepy.jws import Header
+        self.header1 = Header(jwk='foo')
+        self.header2 = Header(jwk='bar')
+        self.crit = Header(crit=('a', 'b'))
+        self.empty = Header()
+
+    def test_add_non_empty(self):
+        from josepy.jws import Header
+        self.assertEqual(Header(jwk='foo', crit=('a', 'b')),
+                         self.header1 + self.crit)
+
+    def test_add_empty(self):
+        self.assertEqual(self.header1, self.header1 + self.empty)
+        self.assertEqual(self.header1, self.empty + self.header1)
+
+    def test_add_overlapping_error(self):
+        self.assertRaises(TypeError, self.header1.__add__, self.header2)
+
+    def test_add_wrong_type_error(self):
+        self.assertRaises(TypeError, self.header1.__add__, 'xxx')
+
+    def test_crit_decode_always_errors(self):
+        from josepy.jws import Header
+        self.assertRaises(errors.DeserializationError, Header.from_json,
+                          {'crit': ['a', 'b']})
+
+    def test_x5c_decoding(self):
+        from josepy.jws import Header
+        header = Header(x5c=(CERT, CERT))
+        jobj = header.to_partial_json()
+        cert_asn1 = OpenSSL.crypto.dump_certificate(
+            OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped)
+        cert_b64 = base64.b64encode(cert_asn1)
+        self.assertEqual(jobj, {'x5c': [cert_b64, cert_b64]})
+        self.assertEqual(header, Header.from_json(jobj))
+        jobj['x5c'][0] = base64.b64encode(b'xxx' + cert_asn1)
+        self.assertRaises(errors.DeserializationError, Header.from_json, jobj)
+
+    def test_find_key(self):
+        self.assertEqual('foo', self.header1.find_key())
+        self.assertEqual('bar', self.header2.find_key())
+        self.assertRaises(errors.Error, self.crit.find_key)
+
+
+class SignatureTest(unittest.TestCase):
+    """Tests for josepy.jws.Signature."""
+
+    def test_from_json(self):
+        from josepy.jws import Header
+        from josepy.jws import Signature
+        self.assertEqual(
+            Signature(signature=b'foo', header=Header(alg=jwa.RS256)),
+            Signature.from_json(
+                {'signature': 'Zm9v', 'header': {'alg': 'RS256'}}))
+
+    def test_from_json_no_alg_error(self):
+        from josepy.jws import Signature
+        self.assertRaises(errors.DeserializationError,
+                          Signature.from_json, {'signature': 'foo'})
+
+
+class JWSTest(unittest.TestCase):
+    """Tests for josepy.jws.JWS."""
+
+    def setUp(self):
+        self.privkey = KEY
+        self.pubkey = self.privkey.public_key()
+
+        from josepy.jws import JWS
+        self.unprotected = JWS.sign(
+            payload=b'foo', key=self.privkey, alg=jwa.RS256)
+        self.protected = JWS.sign(
+            payload=b'foo', key=self.privkey, alg=jwa.RS256,
+            protect=frozenset(['jwk', 'alg']))
+        self.mixed = JWS.sign(
+            payload=b'foo', key=self.privkey, alg=jwa.RS256,
+            protect=frozenset(['alg']))
+
+    def test_pubkey_jwk(self):
+        self.assertEqual(self.unprotected.signature.combined.jwk, self.pubkey)
+        self.assertEqual(self.protected.signature.combined.jwk, self.pubkey)
+        self.assertEqual(self.mixed.signature.combined.jwk, self.pubkey)
+
+    def test_sign_unprotected(self):
+        self.assertTrue(self.unprotected.verify())
+
+    def test_sign_protected(self):
+        self.assertTrue(self.protected.verify())
+
+    def test_sign_mixed(self):
+        self.assertTrue(self.mixed.verify())
+
+    def test_compact_lost_unprotected(self):
+        compact = self.mixed.to_compact()
+        self.assertEqual(
+            b'eyJhbGciOiAiUlMyNTYifQ.Zm9v.OHdxFVj73l5LpxbFp1AmYX4yJM0Pyb'
+            b'_893n1zQjpim_eLS5J1F61lkvrCrCDErTEJnBGOGesJ72M7b6Ve1cAJA',
+            compact)
+
+        from josepy.jws import JWS
+        mixed = JWS.from_compact(compact)
+
+        self.assertNotEqual(self.mixed, mixed)
+        self.assertEqual(
+            set(['alg']), set(mixed.signature.combined.not_omitted()))
+
+    def test_from_compact_missing_components(self):
+        from josepy.jws import JWS
+        self.assertRaises(errors.DeserializationError, JWS.from_compact, b'.')
+
+    def test_json_omitempty(self):
+        protected_jobj = self.protected.to_partial_json(flat=True)
+        unprotected_jobj = self.unprotected.to_partial_json(flat=True)
+
+        self.assertTrue('protected' not in unprotected_jobj)
+        self.assertTrue('header' not in protected_jobj)
+
+        unprotected_jobj['header'] = unprotected_jobj['header'].to_json()
+
+        from josepy.jws import JWS
+        self.assertEqual(JWS.from_json(protected_jobj), self.protected)
+        self.assertEqual(JWS.from_json(unprotected_jobj), self.unprotected)
+
+    def test_json_flat(self):
+        jobj_to = {
+            'signature': json_util.encode_b64jose(
+                self.mixed.signature.signature),
+            'payload': json_util.encode_b64jose(b'foo'),
+            'header': self.mixed.signature.header,
+            'protected': json_util.encode_b64jose(
+                self.mixed.signature.protected.encode('utf-8')),
+        }
+        jobj_from = jobj_to.copy()
+        jobj_from['header'] = jobj_from['header'].to_json()
+
+        self.assertEqual(self.mixed.to_partial_json(flat=True), jobj_to)
+        from josepy.jws import JWS
+        self.assertEqual(self.mixed, JWS.from_json(jobj_from))
+
+    def test_json_not_flat(self):
+        jobj_to = {
+            'signatures': (self.mixed.signature,),
+            'payload': json_util.encode_b64jose(b'foo'),
+        }
+        jobj_from = jobj_to.copy()
+        jobj_from['signatures'] = [jobj_to['signatures'][0].to_json()]
+
+        self.assertEqual(self.mixed.to_partial_json(flat=False), jobj_to)
+        from josepy.jws import JWS
+        self.assertEqual(self.mixed, JWS.from_json(jobj_from))
+
+    def test_from_json_mixed_flat(self):
+        from josepy.jws import JWS
+        self.assertRaises(errors.DeserializationError, JWS.from_json,
+                          {'signatures': (), 'signature': 'foo'})
+
+    def test_from_json_hashable(self):
+        from josepy.jws import JWS
+        hash(JWS.from_json(self.mixed.to_json()))
+
+
+class CLITest(unittest.TestCase):
+
+    def setUp(self):
+        self.key_path = test_util.vector_path('rsa512_key.pem')
+
+    def test_unverified(self):
+        from josepy.jws import CLI
+        with mock.patch('sys.stdin') as sin:
+            sin.read.return_value = '{"payload": "foo", "signature": "xxx"}'
+            with mock.patch('sys.stdout'):
+                self.assertEqual(-1, CLI.run(['verify']))
+
+    def test_json(self):
+        from josepy.jws import CLI
+
+        with mock.patch('sys.stdin') as sin:
+            sin.read.return_value = 'foo'
+            with mock.patch('sys.stdout') as sout:
+                CLI.run(['sign', '-k', self.key_path, '-a', 'RS256',
+                         '-p', 'jwk'])
+                sin.read.return_value = sout.write.mock_calls[0][1][0]
+                self.assertEqual(0, CLI.run(['verify']))
+
+    def test_compact(self):
+        from josepy.jws import CLI
+
+        with mock.patch('sys.stdin') as sin:
+            sin.read.return_value = 'foo'
+            with mock.patch('sys.stdout') as sout:
+                CLI.run(['--compact', 'sign', '-k', self.key_path])
+                sin.read.return_value = sout.write.mock_calls[0][1][0]
+                self.assertEqual(0, CLI.run([
+                    '--compact', 'verify', '--kty', 'RSA',
+                    '-k', self.key_path]))
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 97 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/test_util.py

@@ -0,0 +1,97 @@
+"""Test utilities.
+
+.. warning:: This module is not part of the public API.
+
+"""
+import os
+import unittest
+
+import OpenSSL
+import pkg_resources
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+
+from josepy import ComparableRSAKey, ComparableX509
+
+
+def vector_path(*names):
+    """Path to a test vector."""
+    return pkg_resources.resource_filename(
+        __name__, os.path.join('testdata', *names))
+
+
+def load_vector(*names):
+    """Load contents of a test vector."""
+    # luckily, resource_string opens file in binary mode
+    return pkg_resources.resource_string(
+        __name__, os.path.join('testdata', *names))
+
+
+def _guess_loader(filename, loader_pem, loader_der):
+    _, ext = os.path.splitext(filename)
+    if ext.lower() == '.pem':
+        return loader_pem
+    elif ext.lower() == '.der':
+        return loader_der
+    else:  # pragma: no cover
+        raise ValueError("Loader could not be recognized based on extension")
+
+
+def load_cert(*names):
+    """Load certificate."""
+    loader = _guess_loader(
+        names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
+    return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
+
+
+def load_comparable_cert(*names):
+    """Load ComparableX509 cert."""
+    return ComparableX509(load_cert(*names))
+
+
+def load_csr(*names):
+    """Load certificate request."""
+    loader = _guess_loader(
+        names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
+    return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
+
+
+def load_comparable_csr(*names):
+    """Load ComparableX509 certificate request."""
+    return ComparableX509(load_csr(*names))
+
+
+def load_rsa_private_key(*names):
+    """Load RSA private key."""
+    loader = _guess_loader(names[-1], serialization.load_pem_private_key,
+                           serialization.load_der_private_key)
+    return ComparableRSAKey(loader(
+        load_vector(*names), password=None, backend=default_backend()))
+
+
+def load_pyopenssl_private_key(*names):
+    """Load pyOpenSSL private key."""
+    loader = _guess_loader(
+        names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
+    return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
+
+
+def skip_unless(condition, reason):  # pragma: no cover
+    """Skip tests unless a condition holds.
+
+    This implements the basic functionality of unittest.skipUnless
+    which is only available on Python 2.7+.
+
+    :param bool condition: If ``False``, the test will be skipped
+    :param str reason: the reason for skipping the test
+
+    :rtype: callable
+    :returns: decorator that hides tests unless condition is ``True``
+
+    """
+    if hasattr(unittest, "skipUnless"):
+        return unittest.skipUnless(condition, reason)
+    elif condition:
+        return lambda cls: cls
+    else:
+        return lambda cls: None

+ 15 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/README

@@ -0,0 +1,15 @@
+In order for josepy.test_util._guess_loader to work properly, make sure
+to use appropriate extension for vector filenames: .pem for PEM and
+.der for DER.
+
+The following command has been used to generate test keys:
+
+  for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
+
+and for the CSR:
+
+  openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
+
+and for the certificate:
+
+  openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der

+ 44 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert-100sans.pem

@@ -0,0 +1,44 @@
+-----BEGIN CERTIFICATE-----
+MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv
+bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X
+DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU
+BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp
+ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B
+AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580
+rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T
+BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t
+ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt
+cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j
+b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN
+ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh
+bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs
+ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx
+LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv
+bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN
+ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh
+bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs
+ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3
+LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv
+bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN
+ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh
+bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs
+ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz
+LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv
+bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN
+ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh
+bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs
+ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5
+LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv
+bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN
+ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh
+bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs
+ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1
+LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv
+bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN
+ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh
+bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs
+ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN
+AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ
+XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG
+-----END CERTIFICATE-----

+ 30 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert-idnsans.pem

@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
+BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv
+bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X
+DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU
+BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp
+ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B
+AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580
+rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T
+BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I
+z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g
+z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z
+z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM
+2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf
+2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3
+2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi
+2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi
+2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs
+aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN
+247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p
+bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4
+27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt
+4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh
+oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh
+oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh
+oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm
+4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2
+LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT
+TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G
+-----END CERTIFICATE-----

+ 14 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert-san.pem

@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx
+ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM
+IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4
+YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG
+A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix
+KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS
+BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR
+7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c
++pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt
+cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF
+nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7
+RDjyGMKy5ZgM2w==
+-----END CERTIFICATE-----

BIN
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert.der


+ 13 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/cert.pem

@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx
+ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM
+IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4
+YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG
+A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix
+KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS
+BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR
+7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c
++pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll
+vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn
+B/o=
+-----END CERTIFICATE-----

+ 28 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/critical-san.pem

@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG
+A1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5
+MRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g
+U2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN
+MTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M
+0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV
+EXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU
+0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl
+XINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF
+fKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD
+VR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl
+bmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV
+HSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo
+dHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw
+SXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0
+dHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ
+KwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv
+DIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w
+DDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L
+ny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs
+OUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst
+bXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS
+yO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be
+n08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv
+KA==
+-----END CERTIFICATE-----

+ 41 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-100sans.pem

@@ -0,0 +1,41 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz
+Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt
+H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6
+lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud
+EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv
+bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh
+bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu
+Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C
+DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4
+YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w
+bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy
+MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j
+b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C
+DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4
+YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w
+bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz
+Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j
+b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C
+DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4
+YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w
+bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1
+My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j
+b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C
+DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4
+YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w
+bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2
+OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j
+b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C
+DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4
+YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w
+bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4
+NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j
+b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C
+DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4
+YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w
+bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3
+DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo
+duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg==
+-----END CERTIFICATE REQUEST-----

+ 12 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-6sans.pem

@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw
+EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy
+c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG
+9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0
+9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG
+9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL
+ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t
+ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd
+k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv
+IvzVBz/nD11drfz/RNuX
+-----END CERTIFICATE REQUEST-----

+ 27 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-idnsans.pem

@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz
+Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt
+H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6
+lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud
+EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP
+iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P
+oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP
+s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ
+jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z
+n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ
+t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC
+YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa
+otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh
+bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb
+jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu
+aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb
+uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg
+reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5
+4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ
+4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ
+4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh
+puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh
+ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr
+dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng==
+-----END CERTIFICATE REQUEST-----

+ 8 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-nosans.pem

@@ -0,0 +1,8 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
+MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt
+cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn
+BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz
+AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo
+wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA=
+-----END CERTIFICATE REQUEST-----

+ 10 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr-san.pem

@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
+EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
+c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG
+9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f
+p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN
+AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t
+MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy
+tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A==
+-----END CERTIFICATE REQUEST-----

BIN
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr.der


+ 10 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/csr.pem

@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
+EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
+c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG
+9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f
+p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN
+AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB
+AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G
+n9XBE1N9W6HCIEut2d8wACg=
+-----END CERTIFICATE REQUEST-----

+ 14 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/dsa512_key.pem

@@ -0,0 +1,14 @@
+-----BEGIN DSA PARAMETERS-----
+MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC
+OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA
+qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl
+41pgNJpgu99YOYqPpS0g7A==
+-----END DSA PARAMETERS-----
+-----BEGIN DSA PRIVATE KEY-----
+MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf
+n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP
+AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm
+rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth
+zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE
+mNv063So6E+eYaIN
+-----END DSA PRIVATE KEY-----

+ 15 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa1024_key.pem

@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi
+4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/
+w4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB
+AoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB
+Ia+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc
+TmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB
+CZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X
+UTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak
+Co9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt
+73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa
+HgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU
+6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ
+c2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc=
+-----END RSA PRIVATE KEY-----

+ 22 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa2048_cert.pem

@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV
+BAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do
+ZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx
+MzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC
+V0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy
+dGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN
+PSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm
+7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn
+xvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD
+g7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1
+RAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU
+uQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G
+A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T
+RiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx
+nkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8
+kTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+
+aonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P
+AfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf
+Esg=
+-----END CERTIFICATE-----

+ 28 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa2048_key.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq
+r5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo
+VpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW
+AEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ
+MmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W
+Gju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp
+v+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3
+3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO
+z7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB
+o35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK
+lt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH
+Vqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a
+hUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/
+IkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/
+9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP
+n94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj
+9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy
+ftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb
+f4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3
+YEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3
+xFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG
+wifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL
+Hh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK
+jUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh
+QxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6
+kmf1UgGFcKrJuXgwEtTVxw==
+-----END PRIVATE KEY-----

+ 6 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa256_key.pem

@@ -0,0 +1,6 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh
+AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N
+E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3
+rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt
+-----END RSA PRIVATE KEY-----

+ 9 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/testdata/rsa512_key.pem

@@ -0,0 +1,9 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79
+vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn
+elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc
+mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp
+Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj
+8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq
+6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/
+-----END RSA PRIVATE KEY-----

+ 227 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/util.py

@@ -0,0 +1,227 @@
+"""JOSE utilities."""
+import collections
+
+import OpenSSL
+import six
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+
+class abstractclassmethod(classmethod):
+    # pylint: disable=invalid-name,too-few-public-methods
+    """Descriptor for an abstract classmethod.
+
+    It augments the :mod:`abc` framework with an abstract
+    classmethod. This is implemented as :class:`abc.abstractclassmethod`
+    in the standard Python library starting with version 3.2.
+
+    This particular implementation, allegedly based on Python 3.3 source
+    code, is stolen from
+    http://stackoverflow.com/questions/11217878/python-2-7-combine-abc-abstractmethod-and-classmethod.
+
+    """
+    __isabstractmethod__ = True
+
+    def __init__(self, target):
+        target.__isabstractmethod__ = True
+        super(abstractclassmethod, self).__init__(target)
+
+
+class ComparableX509(object):  # pylint: disable=too-few-public-methods
+    """Wrapper for OpenSSL.crypto.X509** objects that supports __eq__.
+
+    :ivar wrapped: Wrapped certificate or certificate request.
+    :type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
+
+    """
+    def __init__(self, wrapped):
+        assert isinstance(wrapped, OpenSSL.crypto.X509) or isinstance(
+            wrapped, OpenSSL.crypto.X509Req)
+        self.wrapped = wrapped
+
+    def __getattr__(self, name):
+        return getattr(self.wrapped, name)
+
+    def _dump(self, filetype=OpenSSL.crypto.FILETYPE_ASN1):
+        """Dumps the object into a buffer with the specified encoding.
+
+        :param int filetype: The desired encoding. Should be one of
+            `OpenSSL.crypto.FILETYPE_ASN1`,
+            `OpenSSL.crypto.FILETYPE_PEM`, or
+            `OpenSSL.crypto.FILETYPE_TEXT`.
+
+        :returns: Encoded X509 object.
+        :rtype: str
+
+        """
+        if isinstance(self.wrapped, OpenSSL.crypto.X509):
+            func = OpenSSL.crypto.dump_certificate
+        else:  # assert in __init__ makes sure this is X509Req
+            func = OpenSSL.crypto.dump_certificate_request
+        return func(filetype, self.wrapped)
+
+    def __eq__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        # pylint: disable=protected-access
+        return self._dump() == other._dump()
+
+    def __hash__(self):
+        return hash((self.__class__, self._dump()))
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __repr__(self):
+        return '<{0}({1!r})>'.format(self.__class__.__name__, self.wrapped)
+
+
+class ComparableKey(object):  # pylint: disable=too-few-public-methods
+    """Comparable wrapper for ``cryptography`` keys.
+
+    See https://github.com/pyca/cryptography/issues/2122.
+
+    """
+    __hash__ = NotImplemented
+
+    def __init__(self, wrapped):
+        self._wrapped = wrapped
+
+    def __getattr__(self, name):
+        return getattr(self._wrapped, name)
+
+    def __eq__(self, other):
+        # pylint: disable=protected-access
+        if (not isinstance(other, self.__class__) or
+                self._wrapped.__class__ is not other._wrapped.__class__):
+            return NotImplemented
+        elif hasattr(self._wrapped, 'private_numbers'):
+            return self.private_numbers() == other.private_numbers()
+        elif hasattr(self._wrapped, 'public_numbers'):
+            return self.public_numbers() == other.public_numbers()
+        else:
+            return NotImplemented
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __repr__(self):
+        return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped)
+
+    def public_key(self):
+        """Get wrapped public key."""
+        return self.__class__(self._wrapped.public_key())
+
+
+class ComparableRSAKey(ComparableKey):  # pylint: disable=too-few-public-methods
+    """Wrapper for ``cryptography`` RSA keys.
+
+    Wraps around:
+
+    - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
+    - :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
+
+    """
+
+    def __hash__(self):
+        # public_numbers() hasn't got stable hash!
+        # https://github.com/pyca/cryptography/issues/2143
+        if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization):
+            priv = self.private_numbers()
+            pub = priv.public_numbers
+            return hash((self.__class__, priv.p, priv.q, priv.dmp1,
+                         priv.dmq1, priv.iqmp, pub.n, pub.e))
+        elif isinstance(self._wrapped, rsa.RSAPublicKeyWithSerialization):
+            pub = self.public_numbers()
+            return hash((self.__class__, pub.n, pub.e))
+
+
+class ImmutableMap(collections.Mapping, collections.Hashable):  # type: ignore
+    # pylint: disable=too-few-public-methods
+    """Immutable key to value mapping with attribute access."""
+
+    __slots__ = ()
+    """Must be overridden in subclasses."""
+
+    def __init__(self, **kwargs):
+        if set(kwargs) != set(self.__slots__):
+            raise TypeError(
+                '__init__() takes exactly the following arguments: {0} '
+                '({1} given)'.format(', '.join(self.__slots__),
+                                     ', '.join(kwargs) if kwargs else 'none'))
+        for slot in self.__slots__:
+            object.__setattr__(self, slot, kwargs.pop(slot))
+
+    def update(self, **kwargs):
+        """Return updated map."""
+        items = dict(self)
+        items.update(kwargs)
+        return type(self)(**items)  # pylint: disable=star-args
+
+    def __getitem__(self, key):
+        try:
+            return getattr(self, key)
+        except AttributeError:
+            raise KeyError(key)
+
+    def __iter__(self):
+        return iter(self.__slots__)
+
+    def __len__(self):
+        return len(self.__slots__)
+
+    def __hash__(self):
+        return hash(tuple(getattr(self, slot) for slot in self.__slots__))
+
+    def __setattr__(self, name, value):
+        raise AttributeError("can't set attribute")
+
+    def __repr__(self):
+        return '{0}({1})'.format(self.__class__.__name__, ', '.join(
+            '{0}={1!r}'.format(key, value)
+            for key, value in six.iteritems(self)))
+
+
+class frozendict(collections.Mapping, collections.Hashable):  # type: ignore
+    # pylint: disable=invalid-name,too-few-public-methods
+    """Frozen dictionary."""
+    __slots__ = ('_items', '_keys')
+
+    def __init__(self, *args, **kwargs):
+        if kwargs and not args:
+            items = dict(kwargs)
+        elif len(args) == 1 and isinstance(args[0], collections.Mapping):
+            items = args[0]
+        else:
+            raise TypeError()
+        # TODO: support generators/iterators
+
+        object.__setattr__(self, '_items', items)
+        object.__setattr__(self, '_keys', tuple(sorted(six.iterkeys(items))))
+
+    def __getitem__(self, key):
+        return self._items[key]
+
+    def __iter__(self):
+        return iter(self._keys)
+
+    def __len__(self):
+        return len(self._items)
+
+    def _sorted_items(self):
+        return tuple((key, self[key]) for key in self._keys)
+
+    def __hash__(self):
+        return hash(self._sorted_items())
+
+    def __getattr__(self, name):
+        try:
+            return self._items[name]
+        except KeyError:
+            raise AttributeError(name)
+
+    def __setattr__(self, name, value):
+        raise AttributeError("can't set attribute")
+
+    def __repr__(self):
+        return 'frozendict({0})'.format(', '.join('{0}={1!r}'.format(
+            key, value) for key, value in self._sorted_items()))

+ 199 - 0
desktop/core/ext-py/josepy-1.1.0/src/josepy/util_test.py

@@ -0,0 +1,199 @@
+"""Tests for josepy.util."""
+import functools
+import unittest
+
+import six
+
+from josepy import test_util
+
+
+class ComparableX509Test(unittest.TestCase):
+    """Tests for josepy.util.ComparableX509."""
+
+    def setUp(self):
+        # test_util.load_comparable_{csr,cert} return ComparableX509
+        self.req1 = test_util.load_comparable_csr('csr.pem')
+        self.req2 = test_util.load_comparable_csr('csr.pem')
+        self.req_other = test_util.load_comparable_csr('csr-san.pem')
+
+        self.cert1 = test_util.load_comparable_cert('cert.pem')
+        self.cert2 = test_util.load_comparable_cert('cert.pem')
+        self.cert_other = test_util.load_comparable_cert('cert-san.pem')
+
+    def test_getattr_proxy(self):
+        self.assertTrue(self.cert1.has_expired())
+
+    def test_eq(self):
+        self.assertEqual(self.req1, self.req2)
+        self.assertEqual(self.cert1, self.cert2)
+
+    def test_ne(self):
+        self.assertNotEqual(self.req1, self.req_other)
+        self.assertNotEqual(self.cert1, self.cert_other)
+
+    def test_ne_wrong_types(self):
+        self.assertNotEqual(self.req1, 5)
+        self.assertNotEqual(self.cert1, 5)
+
+    def test_hash(self):
+        self.assertEqual(hash(self.req1), hash(self.req2))
+        self.assertNotEqual(hash(self.req1), hash(self.req_other))
+
+        self.assertEqual(hash(self.cert1), hash(self.cert2))
+        self.assertNotEqual(hash(self.cert1), hash(self.cert_other))
+
+    def test_repr(self):
+        for x509 in self.req1, self.cert1:
+            self.assertEqual(repr(x509),
+                             '<ComparableX509({0!r})>'.format(x509.wrapped))
+
+
+class ComparableRSAKeyTest(unittest.TestCase):
+    """Tests for josepy.util.ComparableRSAKey."""
+
+    def setUp(self):
+        # test_utl.load_rsa_private_key return ComparableRSAKey
+        self.key = test_util.load_rsa_private_key('rsa256_key.pem')
+        self.key_same = test_util.load_rsa_private_key('rsa256_key.pem')
+        self.key2 = test_util.load_rsa_private_key('rsa512_key.pem')
+
+    def test_getattr_proxy(self):
+        self.assertEqual(256, self.key.key_size)
+
+    def test_eq(self):
+        self.assertEqual(self.key, self.key_same)
+
+    def test_ne(self):
+        self.assertNotEqual(self.key, self.key2)
+
+    def test_ne_different_types(self):
+        self.assertNotEqual(self.key, 5)
+
+    def test_ne_not_wrapped(self):
+        # pylint: disable=protected-access
+        self.assertNotEqual(self.key, self.key_same._wrapped)
+
+    def test_ne_no_serialization(self):
+        from josepy.util import ComparableRSAKey
+        self.assertNotEqual(ComparableRSAKey(5), ComparableRSAKey(5))
+
+    def test_hash(self):
+        self.assertTrue(isinstance(hash(self.key), int))
+        self.assertEqual(hash(self.key), hash(self.key_same))
+        self.assertNotEqual(hash(self.key), hash(self.key2))
+
+    def test_repr(self):
+        self.assertTrue(repr(self.key).startswith(
+            '<ComparableRSAKey(<cryptography.hazmat.'))
+
+    def test_public_key(self):
+        from josepy.util import ComparableRSAKey
+        self.assertTrue(isinstance(self.key.public_key(), ComparableRSAKey))
+
+
+class ImmutableMapTest(unittest.TestCase):
+    """Tests for josepy.util.ImmutableMap."""
+
+    def setUp(self):
+        # pylint: disable=invalid-name,too-few-public-methods
+        # pylint: disable=missing-docstring
+        from josepy.util import ImmutableMap
+
+        class A(ImmutableMap):
+            __slots__ = ('x', 'y')
+
+        class B(ImmutableMap):
+            __slots__ = ('x', 'y')
+
+        self.A = A
+        self.B = B
+
+        self.a1 = self.A(x=1, y=2)
+        self.a1_swap = self.A(y=2, x=1)
+        self.a2 = self.A(x=3, y=4)
+        self.b = self.B(x=1, y=2)
+
+    def test_update(self):
+        self.assertEqual(self.A(x=2, y=2), self.a1.update(x=2))
+        self.assertEqual(self.a2, self.a1.update(x=3, y=4))
+
+    def test_get_missing_item_raises_key_error(self):
+        self.assertRaises(KeyError, self.a1.__getitem__, 'z')
+
+    def test_order_of_args_does_not_matter(self):
+        self.assertEqual(self.a1, self.a1_swap)
+
+    def test_type_error_on_missing(self):
+        self.assertRaises(TypeError, self.A, x=1)
+        self.assertRaises(TypeError, self.A, y=2)
+
+    def test_type_error_on_unrecognized(self):
+        self.assertRaises(TypeError, self.A, x=1, z=2)
+        self.assertRaises(TypeError, self.A, x=1, y=2, z=3)
+
+    def test_get_attr(self):
+        self.assertEqual(1, self.a1.x)
+        self.assertEqual(2, self.a1.y)
+        self.assertEqual(1, self.a1_swap.x)
+        self.assertEqual(2, self.a1_swap.y)
+
+    def test_set_attr_raises_attribute_error(self):
+        self.assertRaises(
+            AttributeError, functools.partial(self.a1.__setattr__, 'x'), 10)
+
+    def test_equal(self):
+        self.assertEqual(self.a1, self.a1)
+        self.assertEqual(self.a2, self.a2)
+        self.assertNotEqual(self.a1, self.a2)
+
+    def test_hash(self):
+        self.assertEqual(hash((1, 2)), hash(self.a1))
+
+    def test_unhashable(self):
+        self.assertRaises(TypeError, self.A(x=1, y={}).__hash__)
+
+    def test_repr(self):
+        self.assertEqual('A(x=1, y=2)', repr(self.a1))
+        self.assertEqual('A(x=1, y=2)', repr(self.a1_swap))
+        self.assertEqual('B(x=1, y=2)', repr(self.b))
+        self.assertEqual("B(x='foo', y='bar')", repr(self.B(x='foo', y='bar')))
+
+
+class frozendictTest(unittest.TestCase):  # pylint: disable=invalid-name
+    """Tests for josepy.util.frozendict."""
+
+    def setUp(self):
+        from josepy.util import frozendict
+        self.fdict = frozendict(x=1, y='2')
+
+    def test_init_dict(self):
+        from josepy.util import frozendict
+        self.assertEqual(self.fdict, frozendict({'x': 1, 'y': '2'}))
+
+    def test_init_other_raises_type_error(self):
+        from josepy.util import frozendict
+        # specifically fail for generators...
+        self.assertRaises(TypeError, frozendict, six.iteritems({'a': 'b'}))
+
+    def test_len(self):
+        self.assertEqual(2, len(self.fdict))
+
+    def test_hash(self):
+        self.assertTrue(isinstance(hash(self.fdict), int))
+
+    def test_getattr_proxy(self):
+        self.assertEqual(1, self.fdict.x)
+        self.assertEqual('2', self.fdict.y)
+
+    def test_getattr_raises_attribute_error(self):
+        self.assertRaises(AttributeError, self.fdict.__getattr__, 'z')
+
+    def test_setattr_immutable(self):
+        self.assertRaises(AttributeError, self.fdict.__setattr__, 'z', 3)
+
+    def test_repr(self):
+        self.assertEqual("frozendict(x=1, y='2')", repr(self.fdict))
+
+
+if __name__ == '__main__':
+    unittest.main()  # pragma: no cover

+ 34 - 0
desktop/core/ext-py/josepy-1.1.0/tox.ini

@@ -0,0 +1,34 @@
+[tox]
+envlist =
+    py2{6,7}
+    py3{3,4,5,6}
+
+[testenv]
+commands =
+    py.test {posargs}
+deps:
+    # installs the test dependencies as specified in setup.py
+    .[tests]
+
+[testenv:py26]
+commands =
+    {[testenv]commands}
+deps:
+    cryptography<2.0
+    flake8<3.0
+    pytest-flake8==0.5
+    # installs the test dependencies as specified in setup.py
+    .[tests]
+
+[testenv:py33]
+commands =
+    {[testenv]commands}
+deps:
+    cryptography<2.0
+    flake8<3.0
+    pytest-flake8==0.5
+    # installs the test dependencies as specified in setup.py
+    .[tests]
+
+[flake8]
+ignore = E501

+ 24 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/AUTHORS.rst

@@ -0,0 +1,24 @@
+=======
+Credits
+=======
+
+Development Lead
+----------------
+
+* Tasos Katsoulas <akatsoulas@mozilla.com>
+* John Giannelos <jgiannelos@mozilla.com>
+
+Contributors
+------------
+
+* Will Kahn-Greene (`@willkg <https://github.com/willkg>`_)
+* Peter Bengtsson (`@peterbe <https://github.com/peterbe>`_)
+* Jannis Leidel (`@jezdez <https://github.com/jezdez>`_)
+* Jonathan Claudius (`@claudijd <https://github.com/claudijd>`_)
+* Patrick Uiterwijk (`@puiterwijk <https://github.com/puiterwijk>`_)
+* Dustin J. Mitchell (`@djmitche <https://github.com/djmitche>`_)
+* Giorgos Logiotatidis (`@glogiotatidis <https://github.com/glogiotatidis>`_)
+* Olle Jonsson (`@olleolleolle <https://github.com/olleolleolle>`_)
+* `@GermanoGuerrini <https://github.com/GermanoGuerrini>`_
+* John Paulett (`@johnpaulett <https://github.com/johnpaulett>`_)
+* Andreas Lutro (`@anlutro <https://github.com/anlutro>`_)

+ 133 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/CONTRIBUTING.rst

@@ -0,0 +1,133 @@
+============
+Contributing
+============
+
+Contributions are welcome, and they are greatly appreciated! Every
+little bit helps, and credit will always be given.
+
+You can contribute in many ways:
+
+Types of Contributions
+----------------------
+
+Report Bugs
+~~~~~~~~~~~
+
+Report bugs at `<https://github.com/mozilla/mozilla-django-oidc/issues>`_.
+
+If you are reporting a bug, please include:
+
+* Your operating system name and version.
+* Any details about your local setup that might be helpful in troubleshooting.
+* Detailed steps to reproduce the bug.
+
+Fix Bugs
+~~~~~~~~
+
+Look through the GitHub issues for bugs. Anything tagged with "bug"
+is open to whoever wants to implement it.
+
+Implement Features
+~~~~~~~~~~~~~~~~~~
+
+Look through the GitHub issues for features. Anything tagged with "feature"
+is open to whoever wants to implement it.
+
+Write Documentation
+~~~~~~~~~~~~~~~~~~~
+
+mozilla-django-oidc could always use more documentation, whether as part of the
+official mozilla-django-oidc docs, in docstrings, or even on the web in blog posts,
+articles, and such.
+
+Submit Feedback
+~~~~~~~~~~~~~~~
+
+The best way to send feedback is to file an issue at `<https://github.com/mozilla/mozilla-django-oidc/issues>`_.
+
+If you are proposing a feature:
+
+* Explain in detail how it would work.
+* Keep the scope as narrow as possible, to make it easier to implement.
+* Remember that this is a volunteer-driven project, and that contributions
+  are welcome :)
+
+Get Started!
+------------
+
+Ready to contribute? Here's how to set up `mozilla-django-oidc` for local development.
+
+1. Fork the `mozilla-django-oidc` repo on GitHub.
+2. Clone your fork locally::
+
+       $ git clone git@github.com:your_name_here/mozilla-django-oidc.git
+
+3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
+
+       $ mkvirtualenv mozilla-django-oidc
+       $ cd mozilla-django-oidc/
+       $ python setup.py develop
+
+4. Create a branch for local development::
+
+       $ git checkout -b name-of-your-bugfix-or-feature
+
+   Now you can make your changes locally.
+
+5. When you're done making changes, check that your changes pass flake8 and the
+   tests, including testing other Python versions with tox::
+
+       $ flake8 mozilla_django_oidc tests
+       $ python setup.py test
+       $ tox
+
+   To get flake8 and tox, just pip install them into your virtualenv.
+
+6. Make sure you update ``HISTORY.rst`` with your changes in the following categories
+
+    * Backwards-incompatible changes
+    * Features
+    * Bugs
+
+7. Commit your changes and push your branch to GitHub::
+
+       $ git add .
+       $ git commit -m "Your detailed description of your changes."
+       $ git push origin name-of-your-bugfix-or-feature
+
+8. Submit a pull request through the GitHub website.
+
+Pull Request Guidelines
+-----------------------
+
+Before you submit a pull request, check that it meets these guidelines:
+
+1. The pull request should include tests.
+2. If the pull request adds functionality, the docs should be updated. Put
+   your new functionality into a function with a docstring, and add the
+   feature to the list in README.rst.
+3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check
+   `<https://travis-ci.org/mozilla/mozilla-django-oidc/pull_requests>`_
+   and make sure that the tests pass for all supported Python versions.
+
+Tips
+----
+
+We use tox to run tests::
+
+    $ tox
+
+
+To run a specific environment, use the ``-e`` argument::
+
+    $ tox -e py27-django18
+
+
+You can also run the tests in a virtual environment without tox::
+
+    $ DJANGO_SETTINGS_MODULE=tests.settings django-admin.py test
+
+
+You can specify test modules to run rather than the whole suite::
+
+    $ DJANGO_SETTINGS_MODULE=tests.settings django-admin.py test tests.test_views

+ 146 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/HISTORY.rst

@@ -0,0 +1,146 @@
+.. :changelog:
+
+History
+-------
+
+1.0.0 (unreleased)
+++++++++++++++++++
+
+* Add OIDC_AUTHENTICATION_CALLBACK_URL as a new configuration parameter
+* Fail earlier when JWS algorithm does not OIDC_RP_SIGN_ALGO.
+  Thanks `@anlutro`_
+* RS256 verification through ``settings.OIDC_OP_JWKS_ENDPOINT``
+  Thanks `@GermanoGuerrini`_
+* Refactor OIDCAuthenticationBackend so that token retrieval methods can be overridden in a subclass when you need to.
+
+Backwards-incompatible changes:
+
+* ``OIDC_OP_LOGOUT_URL_METHOD`` takes a ``request`` parameter now.
+* Changed name of ``RefreshIDToken`` middleware to ``SessionRefresh``.
+
+
+.. _`@anlutro`: https://github.com/anlutro
+
+0.6.0 (2018-03-27)
+++++++++++++++++++
+
+* Add e2e tests and automation
+* Add caching for exempt URLs
+* Fix logout when session refresh fails
+
+0.5.0 (2018-01-10)
+++++++++++++++++++
+
+* Add Django 2.0 support
+* Fix tox configuration
+
+Backwards-incompatible changes:
+
+* Drop Django 1.10 support
+
+0.4.2 (2017-11-29)
+++++++++++++++++++
+
+* Fix OIDC_USERNAME_ALGO to actually load dotted import path of callback.
+* Add verify_claims method for advanced authentication checks
+
+0.4.1 (2017-10-25)
+++++++++++++++++++
+
+* Send bytes to josepy. Fixes python3 support.
+
+0.4.0 (2017-10-24)
+++++++++++++++++++
+
+Security issues:
+
+* **High**: Replace python-jose with josepy and use pyca/cryptography instead of pycrypto (CVE-2013-7459).
+
+Backwards-incompatible changes:
+
+* ``OIDC_RP_IDP_SIGN_KEY`` no longer uses the JWK json as ``dict`` but PEM or DER keys instead.
+
+
+0.3.2 (2017-10-03)
+++++++++++++++++++
+
+Features:
+
+* Implement RS256 verification
+  Thanks `@puiterwijk`_
+
+Bugs:
+
+* Use ``settings.OIDC_VERIFY_SSL`` also when validating the token.
+  Thanks `@GermanoGuerrini`_
+* Make OpenID Connect scope configurable.
+  Thanks `@puiterwijk`_
+* Add path host injection unit-test (#171)
+* Revisit OIDC_STORE_{ACCESS,ID}_TOKEN config entries
+* Allow configuration of additional auth parameters
+
+
+.. _`@GermanoGuerrini`: https://github.com/GermanoGuerrini
+.. _`@puiterwijk`: https://github.com/puiterwijk
+
+0.3.1 (2017-06-15)
+++++++++++++++++++
+
+Security issues:
+
+* **Medium**: Sanitize next url for authentication view
+
+0.3.0 (2017-06-13)
+++++++++++++++++++
+
+Security issues:
+
+* **Low**: Logout using POST not GET (#126)
+
+Backwards-incompatible changes:
+
+* The ``settings.SITE_URL`` is no longer used. Instead the absolute URL is
+  derived from the request's ``get_host()``.
+* Only log out by HTTP POST allowed.
+
+Bugs:
+
+* Test suite maintenance (#108, #109, #142)
+
+0.2.0 (2017-06-07)
+++++++++++++++++++
+
+Backwards-incompatible changes:
+
+* Drop support for Django 1.9 (#130)
+
+  If you're using Django 1.9, you should update Django first.
+
+* Move middleware to ``mozilla_django_oidc.middleware`` and
+  change it to use authentication endpoint with ``prompt=none`` (#94)
+
+  You'll need to update your ``MIDDLEWARE_CLASSES``/``MIDDLEWARE``
+  setting accordingly.
+
+* Remove legacy ``base64`` handling of OIDC secret. Now RP secret
+  should be plaintext.
+
+Features:
+
+* Add support for Django 1.11 and Python 3.6 (#85)
+* Update middleware to work with Django 1.10+ (#90)
+* Documentation updates
+* Rework test infrastructure so it's tox-based (#100)
+
+Bugs:
+
+* always decode verified token before ``json.load()`` (#116)
+* always redirect to logout_url even when logged out (#121)
+* Change email matching to be case-insensitive (#102)
+* Allow combining OIDCAuthenticationBackend with other backends (#87)
+* fix is_authenticated usage for Django 1.10+ (#125)
+
+0.1.0 (2016-10-12)
+++++++++++++++++++
+
+* First release on PyPI.

+ 373 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/LICENSE

@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 6 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/MANIFEST.in

@@ -0,0 +1,6 @@
+include AUTHORS.rst
+include CONTRIBUTING.rst
+include HISTORY.rst
+include LICENSE
+include README.rst
+recursive-include mozilla_django_oidc *.html *.png *.gif *js *.css *jpg *jpeg *svg *py

+ 365 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/PKG-INFO

@@ -0,0 +1,365 @@
+Metadata-Version: 1.1
+Name: mozilla-django-oidc
+Version: 1.0.0
+Summary: A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services.
+Home-page: https://github.com/mozilla/mozilla-django-oidc
+Author: Tasos Katsoulas, John Giannelos
+Author-email: akatsoulas@mozilla.com, jgiannelos@mozilla.com
+License: MPL 2.0
+Description: ===================
+        mozilla-django-oidc
+        ===================
+        
+        .. image:: https://badge.fury.io/py/mozilla-django-oidc.svg
+           :target: https://badge.fury.io/py/mozilla-django-oidc
+        
+        .. image:: https://travis-ci.org/mozilla/mozilla-django-oidc.svg?branch=master
+           :target: https://travis-ci.org/mozilla/mozilla-django-oidc
+        
+        .. image:: https://codecov.io/gh/mozilla/mozilla-django-oidc/branch/master/graph/badge.svg
+           :target: https://codecov.io/gh/mozilla/mozilla-django-oidc
+        
+        .. image:: https://circleci.com/gh/mozilla/mozilla-django-oidc/tree/master.svg?style=svg
+           :target: https://circleci.com/gh/mozilla/mozilla-django-oidc/tree/master
+        
+        A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services.
+        
+        
+        Documentation
+        -------------
+        
+        The full documentation is at `<https://mozilla-django-oidc.readthedocs.io>`_.
+        
+        
+        Running Unit Tests
+        -------------------
+        
+        Use ``tox`` to run as many different versions of Python you have. If you
+        don't have ``tox`` installed (and executable) already you can either
+        install it in your system Python or `<https://pypi.python.org/pypi/pipsi>`_.
+        Once installed, simply execute in the project root directory.
+        
+        .. code-block:: shell
+        
+            $ tox
+        
+        ``tox`` will do the equivalent of installing virtual environments for every
+        combination mentioned in the ``tox.ini`` file. If your system, for example,
+        doesn't have ``python3.4`` those ``tox`` tests will be skipped.
+        
+        For a faster test-rinse-repeat cycle you can run tests in a specific
+        environment with a specific version of Python and specific version of
+        Django of your choice. Here is such an example:
+        
+        
+        .. code-block:: shell
+        
+            $ virtualenv -p /path/to/bin/python3.5 venv
+            $ source venv
+            (venv) $ pip install Django==1.11.2
+            (venv) $ pip install -r tests/requirements.txt
+            (venv) $ DJANGO_SETTINGS_MODULE=tests.settings django-admin.py test
+        
+        Measuring code coverage, continuing the steps above:
+        
+        .. code-block:: shell
+        
+            (venv) $ pip install coverage
+            (venv) $ DJANGO_SETTINGS_MODULE=tests.settings coverage run --source mozilla_django_oidc `which django-admin.py` test
+            (venv) $ coverage report
+            (venv) $ coverage html
+            (venv) $ open htmlcov/index.html
+        
+        Local development
+        -----------------
+        
+        The local development setup is based on Docker so you need the following installed in your system:
+        
+        * `docker`
+        * `docker-compose`
+        
+        You will also need to edit your ``hosts`` file to resolve ``testrp`` and ``testprovider`` hostnames to ``127.0.0.1``.
+        
+        Running test services
+        =====================
+        
+        To run the `testrp` and `testprovider` instances run the following:
+        
+        .. code-block:: shell
+        
+           (venv) $ docker-compose up -d testprovider testrp
+        
+        Then visit the testing django app on: ``http://testrp:8081``.
+        
+        The library source code is mounted as a docker volume and source code changes are reflected directly in.
+        In order to test a change you need to restart the ``testrp`` service.
+        
+        .. code-block:: shell
+        
+           (venv) $ docker-compose stop testrp
+           (venv) $ docker-compose up -d testrp
+        
+        Running integration tests
+        =========================
+        
+        Integration tests are mounted as a volume to the docker containers. Tests can be run using the following command:
+        
+        .. code-block:: shell
+        
+           (venv) $ docker-compose run --service-ports testrunner
+        
+        Linting
+        -------
+        
+        All code is checked with `<https://pypi.python.org/pypi/flake8>`_ in
+        continuous integration. To make sure your code still passes all style guides
+        install ``flake8`` and check:
+        
+        .. code-block:: shell
+        
+            $ flake8 mozilla_django_oidc tests
+        
+        .. note::
+        
+            When you run ``tox`` it also does a ``flake8`` run on the main package
+            files and the tests.
+        
+        You can also run linting with ``tox``:
+        
+        .. code-block:: shell
+        
+            $ tox -e lint
+        
+        
+        Releasing a new version
+        ------------------------
+        
+        ``mozilla-django-oidc`` releases are hosted in `PyPI <https://pypi.python.org/pypi/mozilla-django-oidc>`_.
+        Here are the steps you need to follow in order to push a new release:
+        
+        * Make sure that ``HISTORY.rst`` is up-to-date focusing mostly on backwards incompatible changes.
+        
+          Security vulnerabilities should be clearly marked in a "Security issues" section along with
+          a level indicator of:
+        
+          * High: vulnerability facilitates data loss, data access, impersonation of admin, or allows access
+            to other sites or components
+        
+            Users should upgrade immediately.
+        
+          * Medium: vulnerability endangers users by sending them to malicious sites or stealing browser
+            data.
+        
+            Users should upgrade immediately.
+        
+          * Low: vulnerability is a nuissance to site staff and/or users
+        
+            Users should upgrade.
+        
+        * Bump the project version and create a commit for the new version.
+        
+          * You can use ``bumpversion`` for that. It is a tool to automate this procedure following the `semantic versioning scheme <http://semver.org/>`_.
+        
+            * For a patch version update (eg 0.1.1 to 0.1.2) you can run ``bumpversion patch``.
+            * For a minor version update (eg 0.1.0 to 0.2.0) you can run ``bumpversion minor``.
+            * For a major version update (eg 0.1.0 to 1.0.0) you can run ``bumpversion major``.
+        
+        * Create a `signed tag <https://git-scm.com/book/tr/v2/Git-Tools-Signing-Your-Work>`_ for that version
+        
+          Example::
+        
+              git tag -s 0.1.1 -m "Bump version: 0.1.0 to 0.1.1"
+        
+        * Push the signed tag to Github
+        
+          Example::
+        
+              git push origin 0.1.1
+        
+        The release is pushed automatically to PyPI using a travis deployment hook on every new tag.
+        
+        
+        License
+        -------
+        
+        This software is licensed under the MPL 2.0 license. For more info check the LICENSE file.
+        
+        
+        Credits
+        -------
+        
+        Tools used in rendering this package:
+        
+        *  Cookiecutter_
+        *  `cookiecutter-djangopackage`_
+        
+        .. _Cookiecutter: https://github.com/audreyr/cookiecutter
+        .. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage
+        
+        
+        
+        
+        History
+        -------
+        
+        1.0.0 (unreleased)
+        ++++++++++++++++++
+        
+        * Add OIDC_AUTHENTICATION_CALLBACK_URL as a new configuration parameter
+        * Fail earlier when JWS algorithm does not OIDC_RP_SIGN_ALGO.
+          Thanks `@anlutro`_
+        * RS256 verification through ``settings.OIDC_OP_JWKS_ENDPOINT``
+          Thanks `@GermanoGuerrini`_
+        * Refactor OIDCAuthenticationBackend so that token retrieval methods can be overridden in a subclass when you need to.
+        
+        Backwards-incompatible changes:
+        
+        * ``OIDC_OP_LOGOUT_URL_METHOD`` takes a ``request`` parameter now.
+        * Changed name of ``RefreshIDToken`` middleware to ``SessionRefresh``.
+        
+        
+        .. _`@anlutro`: https://github.com/anlutro
+        
+        0.6.0 (2018-03-27)
+        ++++++++++++++++++
+        
+        * Add e2e tests and automation
+        * Add caching for exempt URLs
+        * Fix logout when session refresh fails
+        
+        0.5.0 (2018-01-10)
+        ++++++++++++++++++
+        
+        * Add Django 2.0 support
+        * Fix tox configuration
+        
+        Backwards-incompatible changes:
+        
+        * Drop Django 1.10 support
+        
+        0.4.2 (2017-11-29)
+        ++++++++++++++++++
+        
+        * Fix OIDC_USERNAME_ALGO to actually load dotted import path of callback.
+        * Add verify_claims method for advanced authentication checks
+        
+        0.4.1 (2017-10-25)
+        ++++++++++++++++++
+        
+        * Send bytes to josepy. Fixes python3 support.
+        
+        0.4.0 (2017-10-24)
+        ++++++++++++++++++
+        
+        Security issues:
+        
+        * **High**: Replace python-jose with josepy and use pyca/cryptography instead of pycrypto (CVE-2013-7459).
+        
+        Backwards-incompatible changes:
+        
+        * ``OIDC_RP_IDP_SIGN_KEY`` no longer uses the JWK json as ``dict`` but PEM or DER keys instead.
+        
+        
+        0.3.2 (2017-10-03)
+        ++++++++++++++++++
+        
+        Features:
+        
+        * Implement RS256 verification
+          Thanks `@puiterwijk`_
+        
+        Bugs:
+        
+        * Use ``settings.OIDC_VERIFY_SSL`` also when validating the token.
+          Thanks `@GermanoGuerrini`_
+        * Make OpenID Connect scope configurable.
+          Thanks `@puiterwijk`_
+        * Add path host injection unit-test (#171)
+        * Revisit OIDC_STORE_{ACCESS,ID}_TOKEN config entries
+        * Allow configuration of additional auth parameters
+        
+        
+        .. _`@GermanoGuerrini`: https://github.com/GermanoGuerrini
+        .. _`@puiterwijk`: https://github.com/puiterwijk
+        
+        0.3.1 (2017-06-15)
+        ++++++++++++++++++
+        
+        Security issues:
+        
+        * **Medium**: Sanitize next url for authentication view
+        
+        0.3.0 (2017-06-13)
+        ++++++++++++++++++
+        
+        Security issues:
+        
+        * **Low**: Logout using POST not GET (#126)
+        
+        Backwards-incompatible changes:
+        
+        * The ``settings.SITE_URL`` is no longer used. Instead the absolute URL is
+          derived from the request's ``get_host()``.
+        * Only log out by HTTP POST allowed.
+        
+        Bugs:
+        
+        * Test suite maintenance (#108, #109, #142)
+        
+        0.2.0 (2017-06-07)
+        ++++++++++++++++++
+        
+        Backwards-incompatible changes:
+        
+        * Drop support for Django 1.9 (#130)
+        
+          If you're using Django 1.9, you should update Django first.
+        
+        * Move middleware to ``mozilla_django_oidc.middleware`` and
+          change it to use authentication endpoint with ``prompt=none`` (#94)
+        
+          You'll need to update your ``MIDDLEWARE_CLASSES``/``MIDDLEWARE``
+          setting accordingly.
+        
+        * Remove legacy ``base64`` handling of OIDC secret. Now RP secret
+          should be plaintext.
+        
+        Features:
+        
+        * Add support for Django 1.11 and Python 3.6 (#85)
+        * Update middleware to work with Django 1.10+ (#90)
+        * Documentation updates
+        * Rework test infrastructure so it's tox-based (#100)
+        
+        Bugs:
+        
+        * always decode verified token before ``json.load()`` (#116)
+        * always redirect to logout_url even when logged out (#121)
+        * Change email matching to be case-insensitive (#102)
+        * Allow combining OIDCAuthenticationBackend with other backends (#87)
+        * fix is_authenticated usage for Django 1.10+ (#125)
+        
+        0.1.0 (2016-10-12)
+        ++++++++++++++++++
+        
+        * First release on PyPI.
+        
+Keywords: mozilla-django-oidc
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Framework :: Django
+Classifier: Framework :: Django :: 1.8
+Classifier: Framework :: Django :: 1.11
+Classifier: Framework :: Django :: 2.0
+Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: MacOS
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Natural Language :: English
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6

+ 189 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/README.rst

@@ -0,0 +1,189 @@
+===================
+mozilla-django-oidc
+===================
+
+.. image:: https://badge.fury.io/py/mozilla-django-oidc.svg
+   :target: https://badge.fury.io/py/mozilla-django-oidc
+
+.. image:: https://travis-ci.org/mozilla/mozilla-django-oidc.svg?branch=master
+   :target: https://travis-ci.org/mozilla/mozilla-django-oidc
+
+.. image:: https://codecov.io/gh/mozilla/mozilla-django-oidc/branch/master/graph/badge.svg
+   :target: https://codecov.io/gh/mozilla/mozilla-django-oidc
+
+.. image:: https://circleci.com/gh/mozilla/mozilla-django-oidc/tree/master.svg?style=svg
+   :target: https://circleci.com/gh/mozilla/mozilla-django-oidc/tree/master
+
+A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services.
+
+
+Documentation
+-------------
+
+The full documentation is at `<https://mozilla-django-oidc.readthedocs.io>`_.
+
+
+Running Unit Tests
+-------------------
+
+Use ``tox`` to run as many different versions of Python you have. If you
+don't have ``tox`` installed (and executable) already you can either
+install it in your system Python or `<https://pypi.python.org/pypi/pipsi>`_.
+Once installed, simply execute in the project root directory.
+
+.. code-block:: shell
+
+    $ tox
+
+``tox`` will do the equivalent of installing virtual environments for every
+combination mentioned in the ``tox.ini`` file. If your system, for example,
+doesn't have ``python3.4`` those ``tox`` tests will be skipped.
+
+For a faster test-rinse-repeat cycle you can run tests in a specific
+environment with a specific version of Python and specific version of
+Django of your choice. Here is such an example:
+
+
+.. code-block:: shell
+
+    $ virtualenv -p /path/to/bin/python3.5 venv
+    $ source venv
+    (venv) $ pip install Django==1.11.2
+    (venv) $ pip install -r tests/requirements.txt
+    (venv) $ DJANGO_SETTINGS_MODULE=tests.settings django-admin.py test
+
+Measuring code coverage, continuing the steps above:
+
+.. code-block:: shell
+
+    (venv) $ pip install coverage
+    (venv) $ DJANGO_SETTINGS_MODULE=tests.settings coverage run --source mozilla_django_oidc `which django-admin.py` test
+    (venv) $ coverage report
+    (venv) $ coverage html
+    (venv) $ open htmlcov/index.html
+
+Local development
+-----------------
+
+The local development setup is based on Docker so you need the following installed in your system:
+
+* `docker`
+* `docker-compose`
+
+You will also need to edit your ``hosts`` file to resolve ``testrp`` and ``testprovider`` hostnames to ``127.0.0.1``.
+
+Running test services
+=====================
+
+To run the `testrp` and `testprovider` instances run the following:
+
+.. code-block:: shell
+
+   (venv) $ docker-compose up -d testprovider testrp
+
+Then visit the testing django app on: ``http://testrp:8081``.
+
+The library source code is mounted as a docker volume and source code changes are reflected directly in.
+In order to test a change you need to restart the ``testrp`` service.
+
+.. code-block:: shell
+
+   (venv) $ docker-compose stop testrp
+   (venv) $ docker-compose up -d testrp
+
+Running integration tests
+=========================
+
+Integration tests are mounted as a volume to the docker containers. Tests can be run using the following command:
+
+.. code-block:: shell
+
+   (venv) $ docker-compose run --service-ports testrunner
+
+Linting
+-------
+
+All code is checked with `<https://pypi.python.org/pypi/flake8>`_ in
+continuous integration. To make sure your code still passes all style guides
+install ``flake8`` and check:
+
+.. code-block:: shell
+
+    $ flake8 mozilla_django_oidc tests
+
+.. note::
+
+    When you run ``tox`` it also does a ``flake8`` run on the main package
+    files and the tests.
+
+You can also run linting with ``tox``:
+
+.. code-block:: shell
+
+    $ tox -e lint
+
+
+Releasing a new version
+------------------------
+
+``mozilla-django-oidc`` releases are hosted in `PyPI <https://pypi.python.org/pypi/mozilla-django-oidc>`_.
+Here are the steps you need to follow in order to push a new release:
+
+* Make sure that ``HISTORY.rst`` is up-to-date focusing mostly on backwards incompatible changes.
+
+  Security vulnerabilities should be clearly marked in a "Security issues" section along with
+  a level indicator of:
+
+  * High: vulnerability facilitates data loss, data access, impersonation of admin, or allows access
+    to other sites or components
+
+    Users should upgrade immediately.
+
+  * Medium: vulnerability endangers users by sending them to malicious sites or stealing browser
+    data.
+
+    Users should upgrade immediately.
+
+  * Low: vulnerability is a nuissance to site staff and/or users
+
+    Users should upgrade.
+
+* Bump the project version and create a commit for the new version.
+
+  * You can use ``bumpversion`` for that. It is a tool to automate this procedure following the `semantic versioning scheme <http://semver.org/>`_.
+
+    * For a patch version update (eg 0.1.1 to 0.1.2) you can run ``bumpversion patch``.
+    * For a minor version update (eg 0.1.0 to 0.2.0) you can run ``bumpversion minor``.
+    * For a major version update (eg 0.1.0 to 1.0.0) you can run ``bumpversion major``.
+
+* Create a `signed tag <https://git-scm.com/book/tr/v2/Git-Tools-Signing-Your-Work>`_ for that version
+
+  Example::
+
+      git tag -s 0.1.1 -m "Bump version: 0.1.0 to 0.1.1"
+
+* Push the signed tag to Github
+
+  Example::
+
+      git push origin 0.1.1
+
+The release is pushed automatically to PyPI using a travis deployment hook on every new tag.
+
+
+License
+-------
+
+This software is licensed under the MPL 2.0 license. For more info check the LICENSE file.
+
+
+Credits
+-------
+
+Tools used in rendering this package:
+
+*  Cookiecutter_
+*  `cookiecutter-djangopackage`_
+
+.. _Cookiecutter: https://github.com/audreyr/cookiecutter
+.. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage

+ 1 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/__init__.py

@@ -0,0 +1 @@
+__version__ = '1.0.0'

+ 292 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/auth.py

@@ -0,0 +1,292 @@
+import base64
+import hashlib
+import json
+import logging
+import requests
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.backends import ModelBackend
+from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
+try:
+    from django.urls import reverse
+except ImportError:
+    # Django < 2.0.0
+    from django.core.urlresolvers import reverse
+from django.utils.encoding import force_bytes, smart_text, smart_bytes
+from django.utils.module_loading import import_string
+from django.utils import six
+
+from josepy.jwk import JWK
+from josepy.jws import JWS, Header
+
+from mozilla_django_oidc.utils import absolutify, import_from_settings
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+def default_username_algo(email):
+    """Generate username for the Django user.
+
+    :arg str/unicode email: the email address to use to generate a username
+
+    :returns: str/unicode
+
+    """
+    # bluntly stolen from django-browserid
+    # store the username as a base64 encoded sha224 of the email address
+    # this protects against data leakage because usernames are often
+    # treated as public identifiers (so we can't use the email address).
+    username = base64.urlsafe_b64encode(
+        hashlib.sha1(force_bytes(email)).digest()
+    ).rstrip(b'=')
+
+    return smart_text(username)
+
+
+class OIDCAuthenticationBackend(ModelBackend):
+    """Override Django's authentication."""
+
+    def __init__(self, *args, **kwargs):
+        """Initialize settings."""
+        self.OIDC_OP_TOKEN_ENDPOINT = import_from_settings('OIDC_OP_TOKEN_ENDPOINT')
+        self.OIDC_OP_USER_ENDPOINT = import_from_settings('OIDC_OP_USER_ENDPOINT')
+        self.OIDC_OP_JWKS_ENDPOINT = import_from_settings('OIDC_OP_JWKS_ENDPOINT', None)
+        self.OIDC_RP_CLIENT_ID = import_from_settings('OIDC_RP_CLIENT_ID')
+        self.OIDC_RP_CLIENT_SECRET = import_from_settings('OIDC_RP_CLIENT_SECRET')
+        self.OIDC_RP_SIGN_ALGO = import_from_settings('OIDC_RP_SIGN_ALGO', 'HS256')
+        self.OIDC_RP_IDP_SIGN_KEY = import_from_settings('OIDC_RP_IDP_SIGN_KEY', None)
+
+        if (self.OIDC_RP_SIGN_ALGO.startswith('RS') and
+                (self.OIDC_RP_IDP_SIGN_KEY is None and self.OIDC_OP_JWKS_ENDPOINT is None)):
+            msg = '{} alg requires OIDC_RP_IDP_SIGN_KEY or OIDC_OP_JWKS_ENDPOINT to be configured.'
+            raise ImproperlyConfigured(msg.format(self.OIDC_RP_SIGN_ALGO))
+
+        self.UserModel = get_user_model()
+
+    def filter_users_by_claims(self, claims):
+        """Return all users matching the specified email."""
+        email = claims.get('email')
+        if not email:
+            return self.UserModel.objects.none()
+        return self.UserModel.objects.filter(email__iexact=email)
+
+    def verify_claims(self, claims):
+        """Verify the provided claims to decide if authentication should be allowed."""
+        return True
+
+    def create_user(self, claims):
+        """Return object for a newly created user account."""
+        # bluntly stolen from django-browserid
+        # https://github.com/mozilla/django-browserid/blob/master/django_browserid/auth.py
+
+        username_algo = import_from_settings('OIDC_USERNAME_ALGO', None)
+        email = claims.get('email')
+        if not email:
+            return None
+
+        if username_algo:
+            if isinstance(username_algo, six.string_types):
+                username_algo = import_string(username_algo)
+            username = username_algo(email)
+        else:
+            username = default_username_algo(email)
+
+        return self.UserModel.objects.create_user(username, email)
+
+    def update_user(self, user, claims):
+        """Update existing user with new claims, if necessary save, and return user"""
+        return user
+
+    def _verify_jws(self, payload, key):
+        """Verify the given JWS payload with the given key and return the payload"""
+        jws = JWS.from_compact(payload)
+
+        try:
+            alg = jws.signature.combined.alg.name
+        except KeyError:
+            msg = 'No alg value found in header'
+            raise SuspiciousOperation(msg)
+
+        if alg != self.OIDC_RP_SIGN_ALGO:
+            msg = "The provider algorithm {!r} does not match the client's " \
+                  "OIDC_RP_SIGN_ALGO.".format(alg)
+            raise SuspiciousOperation(msg)
+
+        if isinstance(key, six.string_types):
+            # Use smart_bytes here since the key string comes from settings.
+            jwk = JWK.load(smart_bytes(key))
+        else:
+            # The key is a json returned from the IDP JWKS endpoint.
+            jwk = JWK.from_json(key)
+
+        if not jws.verify(jwk):
+            msg = 'JWS token verification failed.'
+            raise SuspiciousOperation(msg)
+
+        return jws.payload
+
+    def retrieve_matching_jwk(self, token):
+        """Get the signing key by exploring the JWKS endpoint of the OP."""
+        response_jwks = requests.get(
+            self.OIDC_OP_JWKS_ENDPOINT,
+            verify=import_from_settings('OIDC_VERIFY_SSL', True)
+        )
+        response_jwks.raise_for_status()
+        jwks = response_jwks.json()
+
+        # Compute the current header from the given token to find a match
+        jws = JWS.from_compact(token)
+        json_header = jws.signature.protected
+        header = Header.json_loads(json_header)
+
+        key = None
+        for jwk in jwks['keys']:
+            if jwk['alg'] == smart_text(header.alg) and jwk['kid'] == smart_text(header.kid):
+                key = jwk
+        if key is None:
+            raise SuspiciousOperation('Could not find a valid JWKS.')
+        return key
+
+    def verify_token(self, token, **kwargs):
+        """Validate the token signature."""
+        nonce = kwargs.get('nonce')
+
+        token = force_bytes(token)
+        if self.OIDC_RP_SIGN_ALGO.startswith('RS'):
+            if self.OIDC_RP_IDP_SIGN_KEY is not None:
+                key = self.OIDC_RP_IDP_SIGN_KEY
+            else:
+                key = self.retrieve_matching_jwk(token)
+        else:
+            key = self.OIDC_RP_CLIENT_SECRET
+
+        # Verify the token
+        verified_token = self._verify_jws(token, key)
+
+        # The 'verified_token' will always be a byte string since it's
+        # the result of base64.urlsafe_b64decode().
+        # The payload is always the result of base64.urlsafe_b64decode().
+        # In Python 3 and 2, that's always a byte string.
+        # In Python3.6, the json.loads() function can accept a byte string
+        # as it will automagically decode it to a unicode string before
+        # deserializing https://bugs.python.org/issue17909
+        verified_id = json.loads(verified_token.decode('utf-8'))
+        token_nonce = verified_id.get('nonce')
+
+        if import_from_settings('OIDC_USE_NONCE', True) and nonce != token_nonce:
+            msg = 'JWT Nonce verification failed.'
+            raise SuspiciousOperation(msg)
+        return verified_id
+
+    def get_token(self, payload):
+        """Return token object as a dictionary."""
+
+        response = requests.post(
+            self.OIDC_OP_TOKEN_ENDPOINT,
+            data=payload,
+            verify=import_from_settings('OIDC_VERIFY_SSL', True))
+        response.raise_for_status()
+        return response.json()
+
+    def get_userinfo(self, access_token, id_token, verified_id):
+        """Return user details dictionary. The id_token and verified_id are not used in
+        the default implementation, but may be used when overriding this method"""
+
+        user_response = requests.get(
+            self.OIDC_OP_USER_ENDPOINT,
+            headers={
+                'Authorization': 'Bearer {0}'.format(access_token)
+            },
+            verify=import_from_settings('OIDC_VERIFY_SSL', True))
+        user_response.raise_for_status()
+        return user_response.json()
+
+    def authenticate(self, **kwargs):
+        """Authenticates a user based on the OIDC code flow."""
+
+        self.request = kwargs.pop('request', None)
+        if not self.request:
+            return None
+
+        state = self.request.GET.get('state')
+        code = self.request.GET.get('code')
+        nonce = kwargs.pop('nonce', None)
+
+        if not code or not state:
+            return None
+
+        reverse_url = import_from_settings('OIDC_AUTHENTICATION_CALLBACK_URL',
+                                           'oidc_authentication_callback')
+
+        token_payload = {
+            'client_id': self.OIDC_RP_CLIENT_ID,
+            'client_secret': self.OIDC_RP_CLIENT_SECRET,
+            'grant_type': 'authorization_code',
+            'code': code,
+            'redirect_uri': absolutify(
+                self.request,
+                reverse(reverse_url)
+            ),
+        }
+
+        # Get the token
+        token_info = self.get_token(token_payload)
+        id_token = token_info.get('id_token')
+        access_token = token_info.get('access_token')
+
+        # Validate the token
+        verified_id = self.verify_token(id_token, nonce=nonce)
+
+        if verified_id:
+            return self.get_or_create_user(access_token, id_token, verified_id)
+
+        return None
+
+    def get_or_create_user(self, access_token, id_token, verified_id):
+        """Returns a User instance if 1 user is found. Creates a user if not found
+        and configured to do so. Returns nothing if multiple users are matched."""
+
+        session = self.request.session
+
+        if import_from_settings('OIDC_STORE_ACCESS_TOKEN', False):
+            session['oidc_access_token'] = access_token
+
+        if import_from_settings('OIDC_STORE_ID_TOKEN', False):
+            session['oidc_id_token'] = id_token
+
+        # get userinfo
+        user_info = self.get_userinfo(access_token, id_token, verified_id)
+
+        email = user_info.get('email')
+
+        claims_verified = self.verify_claims(user_info)
+        if not claims_verified:
+            LOGGER.debug('Login failed: Claims verification for %s failed.', email)
+            return None
+
+        # email based filtering
+        users = self.filter_users_by_claims(user_info)
+
+        if len(users) == 1:
+            return self.update_user(users[0], user_info)
+        elif len(users) > 1:
+            # In the rare case that two user accounts have the same email address,
+            # log and bail. Randomly selecting one seems really wrong.
+            LOGGER.warn('Multiple users with email address %s.', email)
+            return None
+        elif import_from_settings('OIDC_CREATE_USER', True):
+            user = self.create_user(user_info)
+            return user
+        else:
+            LOGGER.debug('Login failed: No user with email %s found, and '
+                         'OIDC_CREATE_USER is False', email)
+            return None
+
+    def get_user(self, user_id):
+        """Return a user based on the id."""
+
+        try:
+            return self.UserModel.objects.get(pk=user_id)
+        except self.UserModel.DoesNotExist:
+            return None

+ 139 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/middleware.py

@@ -0,0 +1,139 @@
+import logging
+import time
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    # Python < 3
+    from urllib import urlencode
+
+import django
+try:
+    from django.urls import reverse
+except ImportError:
+    # Django < 2.0.0
+    from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect, JsonResponse
+from django.utils.crypto import get_random_string
+from django.utils.functional import cached_property
+
+from mozilla_django_oidc.utils import (
+    absolutify,
+    import_from_settings,
+    is_authenticated
+)
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+# Django 1.10 makes changes to how middleware work. In Django 1.10+, we want to
+# use the mixin so that our middleware works as is.
+if django.VERSION >= (1, 10):
+    from django.utils.deprecation import MiddlewareMixin
+else:
+    class MiddlewareMixin(object):
+        pass
+
+
+class SessionRefresh(MiddlewareMixin):
+    """Refreshes the session with the OIDC RP after expiry seconds
+
+    For users authenticated with the OIDC RP, verify tokens are still valid and
+    if not, force the user to re-authenticate silently.
+
+    """
+
+    @cached_property
+    def exempt_urls(self):
+        """Generate and return a set of url paths to exempt from SessionRefresh
+
+        This takes the value of ``settings.OIDC_EXEMPT_URLS`` and appends three
+        urls that mozilla-django-oidc uses. These values can be view names or
+        absolute url paths.
+
+        :returns: list of url paths (for example "/oidc/callback/")
+
+        """
+        exempt_urls = list(import_from_settings('OIDC_EXEMPT_URLS', []))
+        exempt_urls.extend([
+            'oidc_authentication_init',
+            'oidc_authentication_callback',
+            'oidc_logout',
+        ])
+
+        return set([
+            url if url.startswith('/') else reverse(url)
+            for url in exempt_urls
+        ])
+
+    def is_refreshable_url(self, request):
+        """Takes a request and returns whether it triggers a refresh examination
+
+        :arg HttpRequest request:
+
+        :returns: boolean
+
+        """
+        return (
+            request.method == 'GET' and
+            is_authenticated(request.user) and
+            request.path not in self.exempt_urls
+        )
+
+    def process_request(self, request):
+        if not self.is_refreshable_url(request):
+            LOGGER.debug('request is not refreshable')
+            return
+
+        expiration = request.session.get('oidc_id_token_expiration', 0)
+        now = time.time()
+        if expiration > now:
+            # The id_token is still valid, so we don't have to do anything.
+            LOGGER.debug('id token is still valid (%s > %s)', expiration, now)
+            return
+
+        LOGGER.debug('id token has expired')
+        # The id_token has expired, so we have to re-authenticate silently.
+        auth_url = import_from_settings('OIDC_OP_AUTHORIZATION_ENDPOINT')
+        client_id = import_from_settings('OIDC_RP_CLIENT_ID')
+        state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32))
+
+        # Build the parameters as if we were doing a real auth handoff, except
+        # we also include prompt=none.
+        params = {
+            'response_type': 'code',
+            'client_id': client_id,
+            'redirect_uri': absolutify(
+                request,
+                reverse('oidc_authentication_callback')
+            ),
+            'state': state,
+            'scope': import_from_settings('OIDC_RP_SCOPES', 'openid email'),
+            'prompt': 'none',
+        }
+
+        if import_from_settings('OIDC_USE_NONCE', True):
+            nonce = get_random_string(import_from_settings('OIDC_NONCE_SIZE', 32))
+            params.update({
+                'nonce': nonce
+            })
+            request.session['oidc_nonce'] = nonce
+
+        request.session['oidc_state'] = state
+        request.session['oidc_login_next'] = request.get_full_path()
+
+        query = urlencode(params)
+        redirect_url = '{url}?{query}'.format(url=auth_url, query=query)
+        if request.is_ajax():
+            # Almost all XHR request handling in client-side code struggles
+            # with redirects since redirecting to a page where the user
+            # is supposed to do something is extremely unlikely to work
+            # in an XHR request. Make a special response for these kinds
+            # of requests.
+            # The use of 403 Forbidden is to match the fact that this
+            # middleware doesn't really want the user in if they don't
+            # refresh their session.
+            response = JsonResponse({'refresh_url': redirect_url}, status=403)
+            response['refresh_url'] = redirect_url
+            return response
+        return HttpResponseRedirect(redirect_url)

+ 26 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/urls.py

@@ -0,0 +1,26 @@
+from django.conf.urls import url
+from django.utils.module_loading import import_string
+
+from mozilla_django_oidc import views
+from mozilla_django_oidc.utils import import_from_settings
+
+DEFAULT_CALLBACK_CLASS = 'mozilla_django_oidc.views.OIDCAuthenticationCallbackView'
+CALLBACK_CLASS_PATH = import_from_settings('OIDC_CALLBACK_CLASS', DEFAULT_CALLBACK_CLASS)
+
+OIDCCallbackClass = import_string(CALLBACK_CLASS_PATH)
+
+
+DEFAULT_AUTHENTICATE_CLASS = 'mozilla_django_oidc.views.OIDCAuthenticationRequestView'
+AUTHENTICATE_CLASS_PATH = import_from_settings(
+    'OIDC_AUTHENTICATE_CLASS', DEFAULT_AUTHENTICATE_CLASS
+)
+
+OIDCAuthenticateClass = import_string(AUTHENTICATE_CLASS_PATH)
+
+urlpatterns = [
+    url(r'^callback/$', OIDCCallbackClass.as_view(),
+        name='oidc_authentication_callback'),
+    url(r'^authenticate/$', OIDCAuthenticateClass.as_view(),
+        name='oidc_authentication_init'),
+    url(r'^logout/$', views.OIDCLogoutView.as_view(), name='oidc_logout'),
+]

+ 41 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/utils.py

@@ -0,0 +1,41 @@
+from django import VERSION
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+
+
+def import_from_settings(attr, *args):
+    """
+    Load an attribute from the django settings.
+
+    :raises:
+        ImproperlyConfigured
+    """
+    try:
+        if args:
+            return getattr(settings, attr, args[0])
+        return getattr(settings, attr)
+    except AttributeError:
+        raise ImproperlyConfigured('Setting {0} not found'.format(attr))
+
+
+def absolutify(request, path):
+    """Return the absolute URL of a path."""
+    return request.build_absolute_uri(path)
+
+
+# Computed once, reused in every request
+_less_than_django_1_10 = VERSION < (1, 10)
+
+
+def is_authenticated(user):
+    """return True if the user is authenticated.
+
+    This is necessary because in Django 1.10 the `user.is_authenticated`
+    stopped being a method and is now a property.
+    Actually `user.is_authenticated()` actually works, thanks to a backwards
+    compat trick in Django. But in Django 2.0 it will cease to work
+    as a callable method.
+    """
+    if _less_than_django_1_10:
+        return user.is_authenticated()
+    return user.is_authenticated

+ 195 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/mozilla_django_oidc/views.py

@@ -0,0 +1,195 @@
+import time
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    # Python < 3
+    from urllib import urlencode
+
+import django
+from django.core.exceptions import SuspiciousOperation
+try:
+    from django.urls import reverse
+except ImportError:
+    # Django < 2.0.0
+    from django.core.urlresolvers import reverse
+from django.contrib import auth
+from django.http import HttpResponseRedirect
+from django.utils.crypto import get_random_string
+from django.utils.http import is_safe_url
+from django.utils.module_loading import import_string
+from django.views.generic import View
+
+from mozilla_django_oidc.utils import (
+    absolutify,
+    import_from_settings,
+    is_authenticated
+)
+
+
+class OIDCAuthenticationCallbackView(View):
+    """OIDC client authentication callback HTTP endpoint"""
+
+    http_method_names = ['get']
+
+    @property
+    def failure_url(self):
+        return import_from_settings('LOGIN_REDIRECT_URL_FAILURE', '/')
+
+    @property
+    def success_url(self):
+        # Pull the next url from the session or settings--we don't need to
+        # sanitize here because it should already have been sanitized.
+        next_url = self.request.session.get('oidc_login_next', None)
+        return next_url or import_from_settings('LOGIN_REDIRECT_URL', '/')
+
+    def login_failure(self):
+        return HttpResponseRedirect(self.failure_url)
+
+    def login_success(self):
+        auth.login(self.request, self.user)
+
+        # Figure out when this id_token will expire. This is ignored unless you're
+        # using the RenewIDToken middleware.
+        expiration_interval = import_from_settings('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 60 * 15)
+        self.request.session['oidc_id_token_expiration'] = time.time() + expiration_interval
+
+        return HttpResponseRedirect(self.success_url)
+
+    def get(self, request):
+        """Callback handler for OIDC authorization code flow"""
+
+        nonce = request.session.get('oidc_nonce')
+        if nonce:
+            # Make sure that nonce is not used twice
+            del request.session['oidc_nonce']
+
+        if request.GET.get('error'):
+            # Ouch! Something important failed.
+            # Make sure the user doesn't get to continue to be logged in
+            # otherwise the refresh middleware will force the user to
+            # redirect to authorize again if the session refresh has
+            # expired.
+            if is_authenticated(request.user):
+                auth.logout(request)
+            assert not is_authenticated(request.user)
+        elif 'code' in request.GET and 'state' in request.GET:
+            kwargs = {
+                'request': request,
+                'nonce': nonce,
+            }
+
+            if 'oidc_state' not in request.session:
+                return self.login_failure()
+
+            if request.GET['state'] != request.session['oidc_state']:
+                msg = 'Session `oidc_state` does not match the OIDC callback state'
+                raise SuspiciousOperation(msg)
+
+            self.user = auth.authenticate(**kwargs)
+
+            if self.user and self.user.is_active:
+                return self.login_success()
+        return self.login_failure()
+
+
+def get_next_url(request, redirect_field_name):
+    """Retrieves next url from request
+
+    Note: This verifies that the url is safe before returning it. If the url
+    is not safe, this returns None.
+
+    :arg HttpRequest request: the http request
+    :arg str redirect_field_name: the name of the field holding the next url
+
+    :returns: safe url or None
+
+    """
+    next_url = request.GET.get(redirect_field_name)
+    if next_url:
+        kwargs = {
+            'url': next_url,
+            'host': request.get_host()
+        }
+        # NOTE(willkg): Django 1.11+ allows us to require https, too.
+        if django.VERSION >= (1, 11):
+            kwargs['require_https'] = request.is_secure()
+        is_safe = is_safe_url(**kwargs)
+        if is_safe:
+            return next_url
+    return None
+
+
+class OIDCAuthenticationRequestView(View):
+    """OIDC client authentication HTTP endpoint"""
+
+    http_method_names = ['get']
+
+    def __init__(self, *args, **kwargs):
+        super(OIDCAuthenticationRequestView, self).__init__(*args, **kwargs)
+
+        self.OIDC_OP_AUTH_ENDPOINT = import_from_settings('OIDC_OP_AUTHORIZATION_ENDPOINT')
+        self.OIDC_RP_CLIENT_ID = import_from_settings('OIDC_RP_CLIENT_ID')
+
+    def get(self, request):
+        """OIDC client authentication initialization HTTP endpoint"""
+        state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32))
+        redirect_field_name = import_from_settings('OIDC_REDIRECT_FIELD_NAME', 'next')
+        reverse_url = import_from_settings('OIDC_AUTHENTICATION_CALLBACK_URL',
+                                           'oidc_authentication_callback')
+
+        params = {
+            'response_type': 'code',
+            'scope': import_from_settings('OIDC_RP_SCOPES', 'openid email'),
+            'client_id': self.OIDC_RP_CLIENT_ID,
+            'redirect_uri': absolutify(
+                request,
+                reverse(reverse_url)
+            ),
+            'state': state,
+        }
+
+        params.update(self.get_extra_params(request))
+
+        if import_from_settings('OIDC_USE_NONCE', True):
+            nonce = get_random_string(import_from_settings('OIDC_NONCE_SIZE', 32))
+            params.update({
+                'nonce': nonce
+            })
+            request.session['oidc_nonce'] = nonce
+
+        request.session['oidc_state'] = state
+        request.session['oidc_login_next'] = get_next_url(request, redirect_field_name)
+
+        query = urlencode(params)
+        redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query)
+        return HttpResponseRedirect(redirect_url)
+
+    def get_extra_params(self, request):
+        return import_from_settings('OIDC_AUTH_REQUEST_EXTRA_PARAMS', {})
+
+
+class OIDCLogoutView(View):
+    """Logout helper view"""
+
+    http_method_names = ['get', 'post']
+
+    @property
+    def redirect_url(self):
+        """Return the logout url defined in settings."""
+        return import_from_settings('LOGOUT_REDIRECT_URL', '/')
+
+    def post(self, request):
+        """Log out the user."""
+        logout_url = self.redirect_url
+
+        if is_authenticated(request.user):
+            # Check if a method exists to build the URL to log out the user
+            # from the OP.
+            logout_from_op = import_from_settings('OIDC_OP_LOGOUT_URL_METHOD', '')
+            if logout_from_op:
+                logout_url = import_string(logout_from_op)(request)
+
+            # Log out the Django user if they were logged in.
+            auth.logout(request)
+
+        return HttpResponseRedirect(logout_url)

+ 18 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/setup.cfg

@@ -0,0 +1,18 @@
+[bumpversion]
+current_version = 1.0.0
+commit = True
+tag = False
+
+[bumpversion:file:mozilla_django_oidc/__init__.py]
+
+[wheel]
+universal = 1
+
+[flake8]
+max-line-length = 99
+exclude = docs,build
+
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 80 - 0
desktop/core/ext-py/mozilla-django-oidc-1.0.0/setup.py

@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import os
+import sys
+
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup
+
+
+from mozilla_django_oidc import __version__ as VERSION
+
+
+if sys.argv[-1] == 'publish':
+    try:
+        import wheel
+        print('Wheel version: ', wheel.__version__)
+    except ImportError:
+        print('Wheel library missing. Please run "pip install wheel"')
+        sys.exit()
+    os.system('python setup.py sdist upload')
+    os.system('python setup.py bdist_wheel upload')
+    sys.exit()
+
+if sys.argv[-1] == 'tag':
+    print('Tagging the version on git:')
+    os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION))
+    os.system('git push --tags')
+    sys.exit()
+
+readme = open('README.rst').read()
+history = open('HISTORY.rst').read().replace('.. :changelog:', '')
+
+install_requirements = [
+    'Django>1.7',
+    'josepy',
+    'requests'
+]
+# cryptography dropped supporting Python 3.2/3.3 at some point
+if sys.version_info[:2] > (2, 7) and sys.version_info[:2] < (3, 4):
+    install_requirements.append('cryptography<1.9')
+else:
+    install_requirements.append('cryptography>1.9')
+
+
+setup(
+    name='mozilla-django-oidc',
+    version=VERSION,
+    description="""A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services.""",  # noqa
+    long_description=readme + '\n\n' + history,
+    author='Tasos Katsoulas, John Giannelos',
+    author_email='akatsoulas@mozilla.com, jgiannelos@mozilla.com',
+    url='https://github.com/mozilla/mozilla-django-oidc',
+    packages=['mozilla_django_oidc'],
+    include_package_data=True,
+    install_requires=install_requirements,
+    license='MPL 2.0',
+    zip_safe=False,
+    keywords='mozilla-django-oidc',
+    classifiers=[
+        'Development Status :: 3 - Alpha',
+        'Framework :: Django',
+        'Framework :: Django :: 1.8',
+        'Framework :: Django :: 1.11',
+        'Framework :: Django :: 2.0',
+        'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
+        'Intended Audience :: Developers',
+        'Operating System :: MacOS',
+        'Operating System :: POSIX :: Linux',
+        'Natural Language :: English',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+    ],
+)

+ 16 - 0
desktop/core/src/desktop/auth/backend.py

@@ -44,6 +44,7 @@ from django_auth_ldap.config import LDAPSearch
 import desktop.conf
 from desktop import metrics
 from liboauth.metrics import oauth_authentication_time
+from mozilla_django_oidc.auth import OIDCAuthenticationBackend
 
 from useradmin import ldap_access
 from useradmin.forms import validate_username
@@ -582,3 +583,18 @@ class RemoteUserDjangoBackend(django.contrib.auth.backends.RemoteUserBackend):
     user = rewrite_user(user)
     return user
 
+
+class OIDCBackend(OIDCAuthenticationBackend):
+  def authenticate(self, **kwargs):
+    user = super(OIDCBackend, self).authenticate(**kwargs)
+    user = rewrite_user(user)
+    return user
+
+  def get_user(self, user_id):
+    user = super(OIDCBackend, self).get_user(user_id)
+    user = rewrite_user(user)
+    return user
+
+  # def filter_users_by_claims(self, claims):
+
+  # def verify_claims(self, claims):

+ 90 - 0
desktop/core/src/desktop/conf.py

@@ -1265,6 +1265,96 @@ OAUTH = ConfigSection(
   )
 )
 
+OIDC = ConfigSection(
+  key='oidc',
+  help=_('Configuration options for OpenID Connect authentication'),
+  members=dict(
+    OIDC_RP_CLIENT_ID = Config(
+      key="oidc_rp_client_id",
+      help=_("The client ID as relay party set in OpenID provider."),
+      type=str,
+      default="XXXXXXXXXXXXXXXXXXXXX"
+    ),
+
+    OIDC_RP_CLIENT_SECRET = Config(
+      key="oidc_rp_client_secret",
+      help=_("The client secret as relay party set in OpenID provider."),
+      type=str,
+      default="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+    ),
+
+    OIDC_OP_AUTHORIZATION_ENDPOINT = Config(
+      key="oidc_op_authorization_endpoint",
+      help=_("The OpenID provider authoriation endpoint."),
+      type=str,
+      default="https://keycloak.example.com/auth/realms/Cloudera/protocol/openid-connect/auth"
+    ),
+
+    OIDC_OP_TOKEN_ENDPOINT = Config(
+      key="oidc_op_token_endpoint",
+      help=_("The OpenID provider token endpoint."),
+      type=str,
+      default="https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/token"
+    ),
+
+    AUTHENTICATE_URL = Config(
+      key="authenticate_url",
+      help=_("The Authorize URL."),
+      type=str,
+      default="https://api.twitter.com/oauth/authorize"
+    ),
+
+    OIDC_OP_USER_ENDPOINT=Config(
+      key="oidc_op_user_endpoint",
+      help=_("The OpenID provider user info endpoint."),
+      type=str,
+      default="https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/userinfo"
+    ),
+
+    OIDC_RP_IDP_SIGN_KEY=Config(
+      key="oidc_rp_idp_sign_key",
+      help=_("The OpenID provider signing key in PEM or DER format."),
+      type=str,
+      default=None
+    ),
+
+    OIDC_OP_JWKS_ENDPOINT=Config(
+      key="oidc_op_jwks_endpoint",
+      help=_("The OpenID provider authoriation endpoint."),
+      type=str,
+      default="https://keycloak.example.com/auth/realms/Cloudera/protocol/openid-connect/certs"
+    ),
+
+    OIDC_VERIFY_SSL=Config(
+      key="oidc_verify_ssl",
+      help=_("Whether Hue as OpenID Connect client verify SSL cert."),
+      type=coerce_bool,
+      default=False
+    ),
+
+    LOGIN_REDIRECT_URL=Config(
+      key="login_redirect_url",
+      help=_("As relay party Hue URL path to redirect to after login."),
+      type=str,
+      default="https://localhost:8888/oidc/callback/"
+    ),
+
+    LOGOUT_REDIRECT_URL=Config(
+      key="logout_redirect_url",
+      help=_("The OpenID provider URL path to redirect to after logout."),
+      type=str,
+      default="https://keycloak.example.com/auth/realms/cloudera/protocol/openid-connect/logout"
+    ),
+
+    LOGIN_REDIRECT_URL_FAILURE=Config(
+      key="login_redirect_url_failure",
+      help=_("As relay party Hue URL path to redirect to after login."),
+      type=str,
+      default="https://localhost:8888/hue/oidc_failed/"
+    ),
+
+  )
+)
 
 LOCAL_FILESYSTEMS = UnspecifiedConfigSection(
   key="local_filesystems",

+ 7 - 0
desktop/core/src/desktop/middleware.py

@@ -274,6 +274,10 @@ class LoginAndPermissionMiddleware(object):
     request.ts = time.time()
     request.view_func = view_func
     access_log_level = getattr(view_func, 'access_log_level', None)
+    # skip loop for oidc
+    if request.path in ['/oidc/authenticate/', '/oidc/callback/', '/oidc/logout/']:
+      return None
+
     # First, skip views not requiring login
 
     # If the view has "opted out" of login required, skip
@@ -659,6 +663,9 @@ class EnsureSafeRedirectURLMiddleware(object):
       if is_safe_url(location, request.get_host()):
         return response
 
+      if request.path in ['/oidc/authenticate/', '/oidc/callback/', '/oidc/logout/']:
+        return response
+
       response = render("error.mako", request, {
         'error': _('Redirect to %s is not allowed.') % response['Location'],
         'is_embeddable': request.GET.get('is_embeddable', False),

+ 23 - 0
desktop/core/src/desktop/settings.py

@@ -457,6 +457,29 @@ if OPENID_AUTHENTICATION:
   LOGIN_URL = '/openid/login'
   SESSION_EXPIRE_AT_BROWSER_CLOSE = True
 
+# OpenID Connect
+def is_oidc_configured():
+  return 'desktop.auth.backend.OIDCBackend' in AUTHENTICATION_BACKENDS
+
+if is_oidc_configured():
+  INSTALLED_APPS.append('mozilla_django_oidc')
+  LOGIN_URL = '/oidc/authenticate/'
+  SESSION_EXPIRE_AT_BROWSER_CLOSE = True
+  MIDDLEWARE_CLASSES.append('mozilla_django_oidc.middleware.SessionRefresh')
+  OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = 15 * 60
+  OIDC_RP_SIGN_ALGO = 'RS256'
+  OIDC_RP_CLIENT_ID = desktop.conf.OIDC.OIDC_RP_CLIENT_ID.get()
+  OIDC_RP_CLIENT_SECRET = desktop.conf.OIDC.OIDC_RP_CLIENT_SECRET.get()
+  OIDC_OP_AUTHORIZATION_ENDPOINT = desktop.conf.OIDC.OIDC_OP_AUTHORIZATION_ENDPOINT.get()
+  OIDC_OP_TOKEN_ENDPOINT = desktop.conf.OIDC.OIDC_OP_TOKEN_ENDPOINT.get()
+  OIDC_OP_USER_ENDPOINT = desktop.conf.OIDC.OIDC_OP_USER_ENDPOINT.get()
+  OIDC_RP_IDP_SIGN_KEY = desktop.conf.OIDC.OIDC_RP_IDP_SIGN_KEY.get()
+  OIDC_OP_JWKS_ENDPOINT = desktop.conf.OIDC.OIDC_OP_JWKS_ENDPOINT.get()
+  OIDC_VERIFY_SSL = desktop.conf.OIDC.OIDC_VERIFY_SSL.get()
+  LOGIN_REDIRECT_URL = desktop.conf.OIDC.LOGIN_REDIRECT_URL.get()
+  LOGOUT_REDIRECT_URL = desktop.conf.OIDC.LOGOUT_REDIRECT_URL.get()
+  LOGIN_REDIRECT_URL_FAILURE = desktop.conf.OIDC.LOGIN_REDIRECT_URL_FAILURE.get()
+
 # OAuth
 OAUTH_AUTHENTICATION='liboauth.backend.OAuthBackend' in AUTHENTICATION_BACKENDS
 if OAUTH_AUTHENTICATION:

+ 6 - 0
desktop/core/src/desktop/urls.py

@@ -39,6 +39,7 @@ from desktop import appmanager
 from desktop.conf import METRICS, USE_NEW_EDITOR, ENABLE_DJANGO_DEBUG_TOOL
 
 from desktop.auth import views as desktop_auth_views
+from desktop.settings import is_oidc_configured
 from desktop import views as desktop_views
 from desktop import api as desktop_api
 from desktop import api2 as desktop_api2
@@ -241,3 +242,8 @@ if settings.DEBUG and ENABLE_DJANGO_DEBUG_TOOL.get():
   urlpatterns += [
     url(r'^__debug__/', include(debug_toolbar.urls)),
   ]
+
+if is_oidc_configured():
+  urlpatterns += [
+    url(r'^oidc/', include('mozilla_django_oidc.urls')),
+  ]