├── examples ├── __init__.py ├── djopenid │ ├── __init__.py │ ├── consumer │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── tests.py │ ├── server │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── tests.py │ ├── templates │ │ ├── consumer │ │ │ ├── request_form.html │ │ │ └── index.html │ │ ├── server │ │ │ ├── endpoint.html │ │ │ ├── idPage.html │ │ │ ├── pape_request_info.html │ │ │ ├── index.html │ │ │ └── trust.html │ │ ├── xrds.xml │ │ └── index.html │ ├── urls.py │ ├── manage.py │ ├── settings.py │ ├── README │ └── util.py ├── discover └── README ├── MANIFEST.in ├── openid ├── extensions │ ├── draft │ │ ├── __init__.py │ │ └── pape5.py │ └── __init__.py ├── test │ ├── data │ │ ├── test_etxrd │ │ │ ├── not-xrds.xml │ │ │ ├── no-xrd.xml │ │ │ ├── status222.xrds │ │ │ ├── README │ │ │ ├── spoof1.xrds │ │ │ ├── spoof2.xrds │ │ │ ├── spoof3.xrds │ │ │ ├── delegated-20060809.xrds │ │ │ ├── valid-populated-xrds.xml │ │ │ ├── delegated-20060809-r1.xrds │ │ │ ├── delegated-20060809-r2.xrds │ │ │ ├── prefixsometimes.xrds │ │ │ ├── sometimesprefix.xrds │ │ │ ├── subsegments.xrds │ │ │ └── ref.xrds │ │ ├── test_discover │ │ │ ├── unicode2.html │ │ │ ├── unicode3.html │ │ │ ├── yadis_no_delegate.xml │ │ │ ├── openid_no_delegate.html │ │ │ ├── openid2_xrds_no_local_id.xml │ │ │ ├── unicode.html │ │ │ ├── yadis_0entries.xml │ │ │ ├── openid2_xrds.xml │ │ │ ├── yadis_idp.xml │ │ │ ├── openid.html │ │ │ ├── openid2.html │ │ │ ├── openid_1_and_2.html │ │ │ ├── yadis_idp_delegate.xml │ │ │ ├── yadis_another_delegate.xml │ │ │ ├── openid_and_yadis.html │ │ │ ├── yadis_2_bad_local_id.xml │ │ │ ├── openid_1_and_2_xrds.xml │ │ │ ├── openid_1_and_2_xrds_bad_delegate.xml │ │ │ ├── yadis_2entries_idp.xml │ │ │ └── yadis_2entries_delegate.xml │ │ ├── openid-1.2-consumer-sqlitestore.db │ │ ├── example-xrds.xml │ │ ├── test1-discover.txt │ │ └── accept.txt │ ├── __init__.py │ ├── test_pape_draft5.py │ ├── test_htmldiscover.py │ ├── test_services.py │ ├── utils.py │ ├── test_symbol.py │ ├── test_extension.py │ ├── test_xrires.py │ ├── test_trustroot.py │ ├── test_nonce.py │ ├── test_accept.py │ ├── test_xri.py │ ├── test_cryptutil.py │ ├── discoverdata.py │ ├── test_urinorm.py │ ├── test_kvform.py │ ├── test_yadis_discover.py │ ├── test_openidyadis.py │ └── test_parsehtml.py ├── consumer │ └── __init__.py ├── server │ └── __init__.py ├── store │ ├── __init__.py │ ├── nonce.py │ └── memstore.py ├── sreg.py ├── constants.py ├── yadis │ ├── constants.py │ ├── __init__.py │ ├── parsehtml.py │ ├── services.py │ ├── xri.py │ ├── xrires.py │ ├── accept.py │ └── discover.py ├── __init__.py ├── extension.py ├── cryptutil.py ├── kvform.py ├── urinorm.py └── oidutil.py ├── .gitattributes ├── admin ├── makechangelog ├── fixperms ├── pythonsource ├── makedoc ├── tagrelease ├── gettlds.py └── builddiscover.py ├── .gitignore ├── setup.cfg ├── .bumpversion.cfg ├── .travis.yml ├── Makefile ├── tox.ini ├── CHANGES-2.2.0 ├── README.md ├── setup.py ├── Changelog.md ├── background-associations.txt └── contrib └── openid-parse /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | -------------------------------------------------------------------------------- /examples/djopenid/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/djopenid/consumer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/djopenid/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openid/extensions/draft/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /Makefile whitespace=space-before-tab,indent-with-non-tab,tabwidth=4 2 | -------------------------------------------------------------------------------- /admin/makechangelog: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | darcs changes --from-tag '^release-' --summary > CHANGELOG 3 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/not-xrds.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/djopenid/server/models.py: -------------------------------------------------------------------------------- 1 | """Required module for Django application.""" 2 | from __future__ import unicode_literals 3 | -------------------------------------------------------------------------------- /examples/djopenid/consumer/models.py: -------------------------------------------------------------------------------- 1 | """Required module for Django application.""" 2 | from __future__ import unicode_literals 3 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/unicode2.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openid/python-openid/HEAD/openid/test/data/test_discover/unicode2.html -------------------------------------------------------------------------------- /openid/test/data/test_discover/unicode3.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openid/python-openid/HEAD/openid/test/data/test_discover/unicode3.html -------------------------------------------------------------------------------- /openid/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | """OpenID Extension modules.""" 2 | from __future__ import unicode_literals 3 | 4 | __all__ = ['ax', 'pape', 'sreg'] 5 | -------------------------------------------------------------------------------- /openid/test/data/openid-1.2-consumer-sqlitestore.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openid/python-openid/HEAD/openid/test/data/openid-1.2-consumer-sqlitestore.db -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | # Distribution 4 | /dist 5 | /*.egg-info 6 | # Tests 7 | /.tox 8 | /.eggs 9 | /sstore 10 | # Coverage 11 | /.coverage* 12 | /htmlcov 13 | -------------------------------------------------------------------------------- /examples/djopenid/templates/consumer/request_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ html|safe }} 4 | 5 | 6 | -------------------------------------------------------------------------------- /admin/fixperms: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cat - < 2 | 7 | 8 | -------------------------------------------------------------------------------- /admin/makedoc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf doc/* 4 | epydoc --html --output doc/ --name "Python-OpenID" --top openid \ 5 | --url "http://openidenabled.com/python-openid/" --inheritance listed \ 6 | --no-private -v \ 7 | $( find openid -name '*.py' -and -not -path 'openid/test*' ) 8 | -------------------------------------------------------------------------------- /openid/store/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains the modules related to this library's use of 3 | persistent storage. 4 | 5 | @sort: interface, filestore, sqlstore, memstore 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | __all__ = ['interface', 'filestore', 'sqlstore', 'memstore', 'nonce'] 10 | -------------------------------------------------------------------------------- /openid/test/__init__.py: -------------------------------------------------------------------------------- 1 | """Openid library tests.""" 2 | from __future__ import unicode_literals 3 | 4 | import unittest 5 | 6 | 7 | # Utility code to allow run unittest under coverage called as module. 8 | def _run_unittest(): 9 | unittest.main() 10 | 11 | 12 | if __name__ == '__main__': 13 | _run_unittest() 14 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/status222.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *x 5 | The subsegment does not exist 6 | 2006-08-18T00:02:35.000Z 7 | xri://= 8 | 9 | -------------------------------------------------------------------------------- /examples/djopenid/templates/server/endpoint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is an OpenID server endpoint. Your browser should never 5 | actually request this page. 6 | 7 | {% if error %} 8 | The OpenID server has encountered an error: 9 |

10 | {{ error|escape }} 11 |

12 | {% endif %} 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_no_delegate.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | http://openid.net/signon/1.0 8 | http://www.myopenid.com/server 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /openid/test/data/example-xrds.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | http://example.com/ 10 | http://www.openidenabled.com/ 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid_no_delegate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Identity Page for Smoker 5 | 6 | 7 | 8 |

foo

9 | 10 | 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | line_length = 120 3 | combine_as_imports = true 4 | default_section = THIRDPARTY 5 | known_first_party = openid 6 | add_imports = from __future__ import unicode_literals 7 | 8 | [flake8] 9 | max-line-length = 120 10 | # Ignore E123 - enforce hang-closing instead 11 | ignore = E123,W503 12 | max-complexity = 24 13 | 14 | [sdist] 15 | force_manifest=1 16 | formats=gztar,zip 17 | -------------------------------------------------------------------------------- /examples/djopenid/consumer/urls.py: -------------------------------------------------------------------------------- 1 | """Consumer URLs.""" 2 | from __future__ import unicode_literals 3 | 4 | from django.conf.urls import url 5 | 6 | from djopenid.consumer.views import finishOpenID, rpXRDS, startOpenID 7 | 8 | urlpatterns = [ 9 | url(r'^$', startOpenID, name='index'), 10 | url(r'^finish/$', finishOpenID, name='return_to'), 11 | url(r'^xrds/$', rpXRDS, name='xrds'), 12 | ] 13 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid2_xrds_no_local_id.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | http://specs.openid.net/auth/2.0/signon 8 | http://www.myopenid.com/server 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/unicode.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Title with param that needs decoding 5 | 6 | 7 |

This page can be properly decoded and everything will will be fine

8 | 9 | 10 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.2 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | parse = (?P\d+)\.(?P\d+)(?P.*) 7 | serialize = 8 | {major}.{minor}{rc} 9 | {major}.{minor} 10 | 11 | [bumpversion:part:rc] 12 | optional_value = final 13 | values = 14 | rc1 15 | rc2 16 | rc3 17 | rc4 18 | rc5 19 | final 20 | 21 | [bumpversion:file:openid/__init__.py] 22 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_0entries.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | http://is-not-openid.unittest/ 9 | http://noffing.unittest./ 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/djopenid/urls.py: -------------------------------------------------------------------------------- 1 | """Djopenid URLs.""" 2 | from __future__ import unicode_literals 3 | 4 | from django.conf.urls import include, url 5 | from django.views.generic import TemplateView 6 | 7 | urlpatterns = [ 8 | url('^$', TemplateView.as_view(template_name='index.html'), name='index'), 9 | url('^consumer/', include(('djopenid.consumer.urls', 'consumer'))), 10 | url('^server/', include(('djopenid.server.urls', 'server'))), 11 | ] 12 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid2_xrds.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | http://specs.openid.net/auth/2.0/signon 8 | http://www.myopenid.com/server 9 | http://smoker.myopenid.com/ 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_idp.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | http://specs.openid.net/auth/2.0/server 9 | http://www.myopenid.com/server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Identity Page for Smoker 5 | 6 | 7 | 8 | 9 |

foo

10 | 11 | 12 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Identity Page for Smoker 5 | 6 | 7 | 8 | 9 |

foo

10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/djopenid/templates/xrds.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | {% for type_uri in type_uris %} 9 | {{ type_uri|escape }} 10 | {% endfor %} 11 | {% for endpoint_url in endpoint_urls %} 12 | {{ endpoint_url }} 13 | {% endfor %} 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /openid/sreg.py: -------------------------------------------------------------------------------- 1 | """moved to L{openid.extensions.sreg}""" 2 | from __future__ import unicode_literals 3 | 4 | import warnings 5 | 6 | from openid.extensions.sreg import SRegRequest, SRegResponse, data_fields, ns_uri, ns_uri_1_0, ns_uri_1_1, supportsSReg 7 | 8 | warnings.warn("openid.sreg has moved to openid.extensions.sreg", 9 | DeprecationWarning) 10 | 11 | __all__ = ['SRegRequest', 'SRegResponse', 'data_fields', 'ns_uri', 'ns_uri_1_0', 'ns_uri_1_1', 'supportsSReg'] 12 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid_1_and_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Identity Page for Smoker 5 | 6 | 7 | 8 | 9 |

foo

10 | 11 | 12 | -------------------------------------------------------------------------------- /admin/tagrelease: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | VERSION="$1" 3 | VERSION_PATTERN='^[0-9]\+\.[0-9]\+\.[0-9]\+\(-[a-z0-9-]\+\)\?$' 4 | 5 | echo "$VERSION" | grep -e "$VERSION_PATTERN" 2>&1 >/dev/null || { 6 | echo "$VERSION" 7 | echo "Malformed version number. Expected X.X.X or X.X.X-name." 1>&2 8 | exit 1 9 | } 10 | 11 | cd $(dirname $(dirname $(readlink --canonicalize "$0"))) 12 | ./admin/setversion "$VERSION" 13 | darcs record -m 'Set version number to '"$VERSION" && darcs tag "release-$VERSION" 14 | -------------------------------------------------------------------------------- /openid/constants.py: -------------------------------------------------------------------------------- 1 | """Basic constants for openid library.""" 2 | from __future__ import unicode_literals 3 | 4 | # Default Diffie-Hellman modulus and generator. 5 | # Defined in OpenID specification http://openid.net/specs/openid-authentication-2_0.html#pvalue 6 | DEFAULT_DH_MODULUS = ('ANz5OguIOXLsDhmYmsWizjEOHTdxfo2Vcbt2I3MYZuYe91ouJ4mLBX+YkcLiemOcPym2CBRYHNOyyjmG0mg3BVd9RcLn5S3I' 7 | 'HHoXGHblzqdLFEi/368Ygo79JRnxTkXjgmY0rxlJ5bU1zIKaSDuKdiI+XUkKJX8Fvf8W8vsixYOr') 8 | DEFAULT_DH_GENERATOR = 'Ag==' 9 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_idp_delegate.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | http://specs.openid.net/auth/2.0/server 9 | http://www.myopenid.com/server 10 | http://smoker.myopenid.com/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/djopenid/templates/server/idPage.html: -------------------------------------------------------------------------------- 1 | {% extends "server/index.html" %} 2 | 3 | {% block head %} 4 | 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 |

12 | This is the identity page for the OpenID that this server serves. 13 |

14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_another_delegate.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | http://openid.net/signon/1.0 10 | http://vroom.unittest/server 11 | http://smoker.myopenid.com/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | # Enable python 3.7 3 | dist: xenial 4 | 5 | sudo: false 6 | 7 | python: 8 | - "2.7" 9 | - "3.5" 10 | - "3.6" 11 | - "3.7" 12 | - "3.8" 13 | - "pypy" 14 | 15 | addons: 16 | apt: 17 | packages: 18 | # Dependencies for pycurl compilation 19 | - libcurl4-openssl-dev 20 | - libssl-dev 21 | 22 | install: 23 | - pip install tox-travis 24 | script: 25 | - tox 26 | after_success: 27 | - coverage combine 28 | - coverage report 29 | - pip install codecov 30 | - codecov 31 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid_and_yadis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Identity Page for Smoker 5 | 6 | 7 | 8 | 9 | 10 |

foo

11 | 12 | 13 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_2_bad_local_id.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | http://specs.openid.net/auth/2.0/signon 10 | http://www.myopenid.com/server 11 | http://smoker.myopenid.com/ 12 | http://localid.mismatch.invalid/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/README: -------------------------------------------------------------------------------- 1 | delegated-20060809.xrds - results from proxy.xri.net, determined by 2 | Drummond and Kevin to be incorrect. 3 | delegated-20060809-r1.xrds - Drummond's 1st correction 4 | delegated-20060809-r2.xrds - Drummond's 2nd correction 5 | 6 | spoofs: keturn's (=!E4)'s attempts to log in with Drummond's i-number (=!D2) 7 | spoof1.xrds 8 | spoof2.xrds 9 | spoof3.xrds - attempt to steal @!C0!D2 by having "at least one" CanonicalID 10 | match the $res service ProviderID. 11 | 12 | ref.xrds - resolving @ootao*test.ref, which refers to a neustar XRI. 13 | -------------------------------------------------------------------------------- /openid/yadis/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from openid.yadis.accept import generateAcceptHeader 4 | 5 | __all__ = ['YADIS_HEADER_NAME', 'YADIS_CONTENT_TYPE', 'YADIS_ACCEPT_HEADER'] 6 | 7 | YADIS_HEADER_NAME = 'X-XRDS-Location' 8 | YADIS_CONTENT_TYPE = 'application/xrds+xml' 9 | 10 | # A value suitable for using as an accept header when performing YADIS 11 | # discovery, unless the application has special requirements 12 | YADIS_ACCEPT_HEADER = generateAcceptHeader( 13 | ('text/html', 0.3), 14 | ('application/xhtml+xml', 0.5), 15 | (YADIS_CONTENT_TYPE, 1.0), 16 | ) 17 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid_1_and_2_xrds.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | http://specs.openid.net/auth/2.0/signon 10 | http://openid.net/signon/1.1 11 | http://www.myopenid.com/server 12 | http://smoker.myopenid.com/ 13 | http://smoker.myopenid.com/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /openid/yadis/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | __all__ = [ 4 | 'constants', 5 | 'discover', 6 | 'etxrd', 7 | 'filters', 8 | 'manager', 9 | 'parsehtml', 10 | 'services', 11 | 'xri', 12 | 'xrires', 13 | ] 14 | 15 | __version__ = '[library version:1.1.0-rc1]'[17:-1] 16 | 17 | # Parse the version info 18 | try: 19 | version_info = tuple(int(i) for i in __version__.split('.')) 20 | except ValueError: 21 | version_info = (None, None, None) 22 | else: 23 | if len(version_info) != 3: 24 | version_info = (None, None, None) 25 | else: 26 | version_info = tuple(version_info) 27 | -------------------------------------------------------------------------------- /examples/djopenid/templates/server/pape_request_info.html: -------------------------------------------------------------------------------- 1 | {% if pape_request %} 2 | {% if pape_request.preferred_auth_policies %} 3 | The relying party requested the following PAPE policies be in effect: 4 | 5 |
    6 | {% for uri in pape_request.preferred_auth_policies %} 7 |
  • {{ uri }}
  • 8 | {% endfor %} 9 |
10 | {% endif %} 11 | 12 | {% if pape_request.preferred_auth_level_types %} 13 | The relying party requested the following authentication level types: 14 | 15 |
    16 | {% for uri in pape_request.preferred_auth_level_types %} 17 |
  • {{ uri }}
  • 18 | {% endfor %} 19 |
20 | {% endif %} 21 | {% endif %} 22 | -------------------------------------------------------------------------------- /examples/djopenid/server/urls.py: -------------------------------------------------------------------------------- 1 | """Server URLs.""" 2 | from __future__ import unicode_literals 3 | 4 | from django.conf.urls import url 5 | from django.views.generic import TemplateView 6 | 7 | from djopenid.server.views import endpoint, idPage, idpXrds, processTrustResult, server 8 | 9 | urlpatterns = [ 10 | url(r'^$', server, name='index'), 11 | url(r'^xrds/$', idpXrds, name='xrds'), 12 | url(r'^user/$', idPage, name='local_id'), 13 | url(r'^endpoint/$', endpoint, name='endpoint'), 14 | url(r'^trust/$', TemplateView.as_view(template_name='server/trust.html'), name='confirmation'), 15 | url(r'^processTrustResult/$', processTrustResult, name='process-confirmation'), 16 | ] 17 | -------------------------------------------------------------------------------- /openid/test/test_pape_draft5.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import unittest 4 | import warnings 5 | 6 | from testfixtures import ShouldWarn 7 | 8 | from openid.extensions import pape 9 | 10 | 11 | class PapeImportTestCase(unittest.TestCase): 12 | def test_version(self): 13 | warning_msg = "Module 'openid.extensions.draft.pape5' is deprecated in favor of 'openid.extensions.pape'." 14 | with ShouldWarn(DeprecationWarning(warning_msg)): 15 | warnings.simplefilter('always') 16 | from openid.extensions.draft import pape5 17 | self.assertEqual(pape.Request, pape5.Request) 18 | self.assertEqual(pape.Response, pape5.Response) 19 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | http://specs.openid.net/auth/2.0/signon 10 | http://openid.net/signon/1.0 11 | http://openid.net/signon/1.1 12 | http://www.myopenid.com/server 13 | http://smoker.myopenid.com/ 14 | http://localid.mismatch.invalid/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test test-openid test-djopenid coverage isort 2 | 3 | SOURCES = openid setup.py admin contrib 4 | 5 | # Run tox by default 6 | all: 7 | tox 8 | 9 | test-openid: 10 | python -m unittest discover --start=openid 11 | 12 | # Run tests for djopenid example 13 | test-djopenid: 14 | DJANGO_SETTINGS_MODULE="djopenid.settings" python -m unittest discover --start=examples 15 | 16 | test: test-openid test-djopenid 17 | 18 | coverage: 19 | python -m coverage erase 20 | -rm -r htmlcov 21 | PYTHONPATH="examples" DJANGO_SETTINGS_MODULE="djopenid.settings" python -m coverage run --branch --source="." openid/test/__init__.py discover 22 | python -m coverage html --directory=htmlcov 23 | 24 | isort: 25 | isort --recursive ${SOURCES} 26 | -------------------------------------------------------------------------------- /openid/test/test_htmldiscover.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import unittest 4 | 5 | from openid.consumer.discover import OpenIDServiceEndpoint 6 | 7 | 8 | class TestFromHTML(unittest.TestCase): 9 | """Test `OpenIDServiceEndpoint.fromHTML`.""" 10 | 11 | def test_empty(self): 12 | self.assertEqual(OpenIDServiceEndpoint.fromHTML('http://example.url/', ''), []) 13 | 14 | def test_invalid_html(self): 15 | self.assertEqual(OpenIDServiceEndpoint.fromHTML('http://example.url/', "http://not.in.a.link.tag/"), []) 16 | 17 | def test_no_op_url(self): 18 | html = '' 19 | self.assertEqual(OpenIDServiceEndpoint.fromHTML('http://example.url/', html), []) 20 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_2entries_idp.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | =!1000 8 | 9 | 10 | http://specs.openid.net/auth/2.0/signon 11 | http://www.myopenid.com/server 12 | http://smoker.myopenid.com/ 13 | 14 | 15 | 16 | http://specs.openid.net/auth/2.0/server 17 | http://www.livejournal.com/openid/server.bml 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /openid/test/test_services.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import unittest 4 | 5 | from openid.yadis import services 6 | from openid.yadis.discover import DiscoveryFailure, DiscoveryResult 7 | 8 | 9 | class TestGetServiceEndpoints(unittest.TestCase): 10 | def setUp(self): 11 | self.orig_discover = services.discover 12 | services.discover = self.discover 13 | 14 | def tearDown(self): 15 | services.discover = self.orig_discover 16 | 17 | def discover(self, input_url): 18 | result = DiscoveryResult(input_url) 19 | result.response_text = "This is not XRDS text." 20 | return result 21 | 22 | def test_catchXRDSError(self): 23 | self.assertRaises(DiscoveryFailure, services.getServiceEndpoints, "http://example.invalid/sometest") 24 | -------------------------------------------------------------------------------- /openid/test/data/test_discover/yadis_2entries_delegate.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | =!1000 8 | 9 | 10 | http://openid.net/signon/1.0 11 | http://www.myopenid.com/server 12 | http://smoker.myopenid.com/ 13 | 14 | 15 | 16 | http://openid.net/signon/1.0 17 | http://www.livejournal.com/openid/server.bml 18 | http://frank.livejournal.com/ 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/spoof1.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *keturn 5 | xri://= 6 | !E4 7 | =!E4 8 | 9 | 10 | xri://$res*auth*($v*2.0) 11 | http://keturn.example.com/resolve/ 12 | =!E4 13 | 14 | 15 | 16 | *isDrummond 17 | =!E4 18 | !D2 19 | =!D2 20 | 21 | http://openid.net/signon/1.0 22 | http://keturn.example.com/openid 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/spoof2.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *keturn 5 | xri://= 6 | !E4 7 | =!E4 8 | 9 | 10 | xri://$res*auth*($v*2.0) 11 | http://keturn.example.com/resolve/ 12 | xri://= 13 | 14 | 15 | 16 | *isDrummond 17 | xri://= 18 | !D2 19 | =!D2 20 | 21 | http://openid.net/signon/1.0 22 | http://keturn.example.com/openid 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /openid/extensions/draft/pape5.py: -------------------------------------------------------------------------------- 1 | """An implementation of the OpenID Provider Authentication Policy 2 | Extension 1.0, Draft 5 3 | 4 | @see: http://openid.net/developers/specs/ 5 | 6 | @since: 2.1.0 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | import warnings 11 | 12 | from openid.extensions.pape import (AUTH_MULTI_FACTOR, AUTH_MULTI_FACTOR_PHYSICAL, AUTH_PHISHING_RESISTANT, LEVELS_JISA, 13 | LEVELS_NIST, Request, Response, ns_uri) 14 | 15 | __all__ = [ 16 | 'Request', 17 | 'Response', 18 | 'ns_uri', 19 | 'AUTH_PHISHING_RESISTANT', 20 | 'AUTH_MULTI_FACTOR', 21 | 'AUTH_MULTI_FACTOR_PHYSICAL', 22 | 'LEVELS_NIST', 23 | 'LEVELS_JISA', 24 | ] 25 | 26 | warnings.warn("Module 'openid.extensions.draft.pape5' is deprecated in favor of 'openid.extensions.pape'.", 27 | DeprecationWarning) 28 | -------------------------------------------------------------------------------- /examples/djopenid/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python OpenID Example 4 | 5 | 6 |

7 | This is a Django package which implements both an OpenID server 8 | and an OpenID consumer. These examples are provided with the 9 | OpenID library so you can learn how to use it in your own 10 | applications. 11 |

12 | 13 |

14 | To begin, click one of these links: 15 |

16 | 17 | 21 | 22 |

23 | Note: If you want to test the example consumer 24 | using the example server, you must start a separate server process 25 | for each application. 26 |

27 | 28 | 29 | -------------------------------------------------------------------------------- /openid/test/utils.py: -------------------------------------------------------------------------------- 1 | """Test utilities.""" 2 | from __future__ import unicode_literals 3 | 4 | from openid import message 5 | 6 | 7 | class OpenIDTestMixin(object): 8 | """Mixin providing custom asserts.""" 9 | 10 | def assertOpenIDValueEqual(self, msg, key, expected, ns=None): 11 | """Check OpenID message contains key with expected value.""" 12 | if ns is None: 13 | ns = message.OPENID_NS 14 | 15 | actual = msg.getArg(ns, key) 16 | error_format = 'Wrong value for openid.%s: expected=%s, actual=%s' 17 | error_message = error_format % (key, expected, actual) 18 | self.assertEqual(actual, expected, error_message) 19 | 20 | def assertOpenIDKeyMissing(self, msg, key, ns=None): 21 | if ns is None: 22 | ns = message.OPENID_NS 23 | 24 | error_message = 'openid.%s unexpectedly present' % key 25 | self.assertFalse(msg.hasKey(ns, key), error_message) 26 | -------------------------------------------------------------------------------- /examples/djopenid/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | import sys 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djopenid.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError: 12 | # The above import may fail for some other reason. Ensure that the 13 | # issue is really that Django is missing to avoid masking other 14 | # exceptions on Python 2. 15 | try: 16 | import django # noqa: F401 17 | except ImportError: 18 | raise ImportError( 19 | "Couldn't import Django. Are you sure it's installed and " 20 | "available on your PYTHONPATH environment variable? Did you " 21 | "forget to activate a virtual environment?" 22 | ) 23 | raise 24 | execute_from_command_line(sys.argv) 25 | -------------------------------------------------------------------------------- /openid/test/test_symbol.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import unittest 4 | 5 | from openid import oidutil 6 | 7 | 8 | class SymbolTest(unittest.TestCase): 9 | def test_selfEquality(self): 10 | s = oidutil.Symbol('xxx') 11 | self.assertEqual(s, s) 12 | 13 | def test_otherEquality(self): 14 | x = oidutil.Symbol('xxx') 15 | y = oidutil.Symbol('xxx') 16 | self.assertEqual(x, y) 17 | 18 | def test_inequality(self): 19 | x = oidutil.Symbol('xxx') 20 | y = oidutil.Symbol('yyy') 21 | self.assertNotEqual(x, y) 22 | 23 | def test_selfInequality(self): 24 | x = oidutil.Symbol('xxx') 25 | self.assertFalse(x != x) 26 | 27 | def test_otherInequality(self): 28 | x = oidutil.Symbol('xxx') 29 | y = oidutil.Symbol('xxx') 30 | self.assertFalse(x != y) 31 | 32 | def test_ne_inequality(self): 33 | x = oidutil.Symbol('xxx') 34 | y = oidutil.Symbol('yyy') 35 | self.assertNotEqual(x, y) 36 | 37 | 38 | if __name__ == '__main__': 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/spoof3.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *keturn 5 | xri://@ 6 | @E4 7 | @!E4 8 | 9 | 10 | xri://$res*auth*($v*2.0) 11 | http://keturn.example.com/resolve/ 12 | @!E4 13 | 14 | 15 | 16 | *is 17 | @!E4 18 | !D2 19 | =!C0 20 | =!E4!01 21 | 22 | xri://$res*auth*($v*2.0) 23 | http://keturn.example.com/resolve/ 24 | @!C0 25 | 26 | 27 | 28 | *drummond 29 | @!C0 30 | !D2 31 | @!C0!D2 32 | 33 | http://openid.net/signon/1.0 34 | http://keturn.example.com/openid 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/djopenid/templates/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Django OpenID Example Server 4 | 29 | {% block head %} 30 | 31 | {% endblock %} 32 | 33 | 34 | 35 | {% block body %} 36 |
37 | 38 |

39 | This is an example server built for the Django framework. It only 40 | authenticates one OpenID, which is also served by this 41 | application. The OpenID it serves is 42 | 43 |

44 | {{ local_id }}
45 |     
46 |

47 | 48 |
49 | {% endblock %} 50 | 51 | 52 | -------------------------------------------------------------------------------- /admin/gettlds.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fetch the current TLD list from the IANA Web site, parse it, and print 3 | an expression suitable for direct insertion into each library's trust 4 | root validation module 5 | 6 | Usage: 7 | python gettlds.py (php|python|ruby) 8 | 9 | Then cut-n-paste. 10 | """ 11 | from __future__ import unicode_literals 12 | 13 | import sys 14 | 15 | import urllib2 16 | 17 | langs = { 18 | 'php': (r"'/\.(", 19 | "'", "|", "|' .", 20 | r")\.?$/'"), 21 | 'python': ("['", 22 | "'", "', '", "',", 23 | "']"), 24 | 'ruby': ("%w'", 25 | "", " ", "", 26 | "'"), 27 | } 28 | 29 | lang = sys.argv[1] 30 | prefix, line_prefix, separator, line_suffix, suffix = langs[lang] 31 | 32 | f = urllib2.urlopen('http://data.iana.org/TLD/tlds-alpha-by-domain.txt') 33 | tlds = [] 34 | output_line = "" 35 | for input_line in f: 36 | if input_line.startswith('#'): 37 | continue 38 | 39 | tld = input_line.strip().lower() 40 | new_output_line = output_line + prefix + tld 41 | if len(new_output_line) > 60: 42 | print(output_line + line_suffix) 43 | output_line = line_prefix + tld 44 | else: 45 | output_line = new_output_line 46 | prefix = separator 47 | 48 | print(output_line + suffix) 49 | -------------------------------------------------------------------------------- /openid/test/test_extension.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import unittest 4 | 5 | from openid import extension, message 6 | 7 | 8 | class DummyExtension(extension.Extension): 9 | ns_uri = 'http://an.extension/' 10 | ns_alias = 'dummy' 11 | 12 | def getExtensionArgs(self): 13 | return {} 14 | 15 | 16 | class ToMessageTest(unittest.TestCase): 17 | def test_OpenID1(self): 18 | oid1_msg = message.Message(message.OPENID1_NS) 19 | ext = DummyExtension() 20 | ext.toMessage(oid1_msg) 21 | namespaces = oid1_msg.namespaces 22 | self.assertTrue(namespaces.isImplicit(DummyExtension.ns_uri)) 23 | self.assertEqual(DummyExtension.ns_uri, namespaces.getNamespaceURI(DummyExtension.ns_alias)) 24 | self.assertEqual(DummyExtension.ns_alias, namespaces.getAlias(DummyExtension.ns_uri)) 25 | 26 | def test_OpenID2(self): 27 | oid2_msg = message.Message(message.OPENID2_NS) 28 | ext = DummyExtension() 29 | ext.toMessage(oid2_msg) 30 | namespaces = oid2_msg.namespaces 31 | self.assertFalse(namespaces.isImplicit(DummyExtension.ns_uri)) 32 | self.assertEqual(DummyExtension.ns_uri, namespaces.getNamespaceURI(DummyExtension.ns_alias)) 33 | self.assertEqual(DummyExtension.ns_alias, namespaces.getAlias(DummyExtension.ns_uri)) 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | quality 4 | py{27,35,36,37,38}-{openid,djopenid,httplib2,pycurl,requests} 5 | pypy-{openid,djopenid,httplib2,pycurl,requests} 6 | 7 | # tox-travis specials 8 | [travis] 9 | python = 10 | 3.8: py38, quality 11 | 12 | # Generic specification for all unspecific environments 13 | [testenv] 14 | extras = 15 | tests 16 | djopenid: djopenid 17 | httplib2: httplib2 18 | pycurl: pycurl 19 | requests: requests 20 | passenv = CI TRAVIS TRAVIS_* 21 | setenv = 22 | DJANGO_SETTINGS_MODULE = djopenid.settings 23 | PYTHONPATH = {toxinidir}/examples:{env:PYTHONPATH:} 24 | # For some reason, python2.7 doesn't always apply `warnings.simplefilter` correctly. Set 'all' as default to avoid. 25 | PYTHONWARNINGS = {env:PYTHONWARNINGS:all} 26 | commands = 27 | coverage run --parallel-mode --branch --source=openid,examples --module unittest discover --start=openid 28 | djopenid: coverage run --parallel-mode --branch --source=openid,examples --module unittest discover --start={toxinidir}/examples 29 | 30 | [testenv:quality] 31 | basepython = python3.8 32 | extras = 33 | quality 34 | commands = 35 | # setup.py is excluded from isort because distutils have problems with unicode_literals. 36 | isort --check-only --diff openid admin contrib 37 | flake8 --format=pylint openid setup.py admin contrib 38 | -------------------------------------------------------------------------------- /examples/djopenid/settings.py: -------------------------------------------------------------------------------- 1 | """Example Django settings for djopenid project.""" 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | import sys 6 | import warnings 7 | 8 | try: 9 | import openid 10 | except ImportError as e: 11 | warnings.warn("Could not import OpenID library. Please consult the djopenid README.") 12 | sys.exit(1) 13 | else: 14 | del openid 15 | 16 | DEBUG = True 17 | ALLOWED_HOSTS = ['*'] 18 | 19 | DATABASES = { 20 | 'default': { 21 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 22 | 'NAME': ':memory:', 23 | } 24 | } 25 | 26 | SECRET_KEY = 'u^bw6lmsa6fah0$^lz-ct$)y7x7#ag92-z+y45-8!(jk0lkavy' 27 | SESSION_ENGINE = 'django.contrib.sessions.backends.file' 28 | SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' 29 | 30 | TEMPLATES = [ 31 | { 32 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 33 | 'DIRS': [os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates'))], 34 | 'APP_DIRS': True, 35 | } 36 | ] 37 | 38 | MIDDLEWARE = ( 39 | 'django.contrib.sessions.middleware.SessionMiddleware', 40 | 'django.middleware.common.CommonMiddleware', 41 | ) 42 | 43 | ROOT_URLCONF = 'djopenid.urls' 44 | 45 | INSTALLED_APPS = ( 46 | 'django.contrib.sessions', 47 | 'djopenid.consumer', 48 | 'djopenid.server', 49 | ) 50 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/delegated-20060809.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *ootao 5 | 6 | 2006-08-09T22:07:13.000Z 7 | xri://@ 8 | !5BAD.2AA.3C72.AF46 9 | @!5BAD.2AA.3C72.AF46 10 | 11 | xri://$res*auth*($v*2.0) 12 | 13 | application/xrds+xml;trust=none 14 | http://resolve.ezibroker.net/resolve/@ootao/ 15 | 16 | 17 | http://openid.net/signon/1.0 18 | 19 | https://linksafe.ezibroker.net/server/ 20 | 21 | 22 | 23 | *test1 24 | SUCCESS 25 | xri://!!1003 26 | !0000.0000.3B9A.CA01 27 | @!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01 28 | 29 | http://openid.net/signon/1.0 30 | 31 | https://linksafe.ezibroker.net/server/ 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/valid-populated-xrds.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | http://openid.net/signon/1.0 11 | http://www.myopenid.com/server 12 | http://josh.myopenid.com/ 13 | 14 | 15 | 16 | http://lid.netmesh.org/sso/2.0b5 17 | http://lid.netmesh.org/2.0b5 18 | http://mylid.net/josh 19 | 20 | 21 | 22 | http://openid.net/signon/1.0 23 | http://www.livejournal.com/openid/server.bml 24 | http://www.livejournal.com/users/nedthealpaca/ 25 | 26 | 27 | 28 | http://typekey.com/services/1.0 29 | joshhoyt 30 | 31 | 32 | 33 | http://openid.net/signon/1.0 34 | http://www.schtuff.com/openid 35 | http://users.schtuff.com/josh 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/delegated-20060809-r1.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *ootao 5 | 6 | 2006-08-09T22:07:13.000Z 7 | xri://@ 8 | !5BAD.2AA.3C72.AF46 9 | @!5BAD.2AA.3C72.AF46 10 | 11 | xri://$res*auth*($v*2.0) 12 | xri://!!1003 13 | application/xrds+xml;trust=none 14 | http://resolve.ezibroker.net/resolve/@ootao/ 15 | 16 | 17 | http://openid.net/signon/1.0 18 | 19 | https://linksafe.ezibroker.net/server/ 20 | 21 | 22 | 23 | *test1 24 | SUCCESS 25 | xri://!!1003 26 | !0000.0000.3B9A.CA01 27 | @!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01 28 | 29 | http://openid.net/signon/1.0 30 | 31 | https://linksafe.ezibroker.net/server/ 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/delegated-20060809-r2.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *ootao 5 | 6 | 2006-08-09T22:07:13.000Z 7 | xri://@ 8 | !5BAD.2AA.3C72.AF46 9 | @!5BAD.2AA.3C72.AF46 10 | 11 | xri://$res*auth*($v*2.0) 12 | xri://@!5BAD.2AA.3C72.AF46 13 | application/xrds+xml;trust=none 14 | http://resolve.ezibroker.net/resolve/@ootao/ 15 | 16 | 17 | http://openid.net/signon/1.0 18 | 19 | https://linksafe.ezibroker.net/server/ 20 | 21 | 22 | 23 | *test1 24 | SUCCESS 25 | xri://@!5BAD.2AA.3C72.AF46 26 | !0000.0000.3B9A.CA01 27 | @!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01 28 | 29 | http://openid.net/signon/1.0 30 | 31 | https://linksafe.ezibroker.net/server/ 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/prefixsometimes.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *ootao 5 | 6 | 2006-08-09T22:07:13.000Z 7 | xri://@ 8 | !5BAD.2AA.3C72.AF46 9 | @!5BAD.2AA.3C72.AF46 10 | 11 | xri://$res*auth*($v*2.0) 12 | xri://@!5BAD.2AA.3C72.AF46 13 | application/xrds+xml;trust=none 14 | http://resolve.ezibroker.net/resolve/@ootao/ 15 | 16 | 17 | http://openid.net/signon/1.0 18 | 19 | https://linksafe.ezibroker.net/server/ 20 | 21 | 22 | 23 | *test1 24 | SUCCESS 25 | xri://@!5BAD.2AA.3C72.AF46 26 | !0000.0000.3B9A.CA01 27 | xri://@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01 28 | 29 | http://openid.net/signon/1.0 30 | 31 | https://linksafe.ezibroker.net/server/ 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/sometimesprefix.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *ootao 5 | 6 | 2006-08-09T22:07:13.000Z 7 | xri://@ 8 | !5BAD.2AA.3C72.AF46 9 | xri://@!5BAD.2AA.3C72.AF46 10 | 11 | xri://$res*auth*($v*2.0) 12 | xri://@!5BAD.2AA.3C72.AF46 13 | application/xrds+xml;trust=none 14 | http://resolve.ezibroker.net/resolve/@ootao/ 15 | 16 | 17 | http://openid.net/signon/1.0 18 | 19 | https://linksafe.ezibroker.net/server/ 20 | 21 | 22 | 23 | *test1 24 | SUCCESS 25 | xri://@!5BAD.2AA.3C72.AF46 26 | !0000.0000.3B9A.CA01 27 | @!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01 28 | 29 | http://openid.net/signon/1.0 30 | 31 | https://linksafe.ezibroker.net/server/ 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /openid/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package is an implementation of the OpenID specification in 3 | Python. It contains code for both server and consumer 4 | implementations. For information on implementing an OpenID consumer, 5 | see the C{L{openid.consumer.consumer}} module. For information on 6 | implementing an OpenID server, see the C{L{openid.server.server}} 7 | module. 8 | 9 | @contact: U{http://openid.net/developers/dev-mailing-lists/ 10 | 14 | 15 | 16 | 17 | http://specs.openid.net/auth/2.0/server 18 | http://example.com/ 19 | 20 | 21 | ''' 22 | 23 | 24 | class FakeFetcher(object): 25 | """Fake fetcher for tests.""" 26 | 27 | def __init__(self): 28 | self.response = None 29 | 30 | def fetch(self, *args, **kwargs): 31 | return self.response 32 | 33 | 34 | class TestStartOpenID(TestCase): 35 | """Test 'startOpenID' view.""" 36 | 37 | def setUp(self): 38 | self.fetcher = FakeFetcher() 39 | setDefaultFetcher(self.fetcher) 40 | 41 | def tearDown(self): 42 | setDefaultFetcher(None) 43 | 44 | def test_get(self): 45 | response = self.client.get('/consumer/') 46 | self.assertContains(response, ' example consumer ') 47 | 48 | def test_post(self): 49 | self.fetcher.response = HTTPResponse('http://example.com/', 200, {'content-type': YADIS_CONTENT_TYPE}, 50 | EXAMPLE_XRDS) 51 | 52 | response = self.client.post('/consumer/', {'openid_identifier': 'http://example.com/'}) 53 | 54 | # Renders a POST form 55 | self.assertContains(response, 'http://example.com/') 56 | self.assertContains(response, 'openid.identity') 57 | -------------------------------------------------------------------------------- /openid/extension.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import warnings 4 | 5 | from openid import message as message_module 6 | 7 | 8 | class Extension(object): 9 | """An interface for OpenID extensions. 10 | 11 | @ivar ns_uri: The namespace to which to add the arguments for this 12 | extension 13 | """ 14 | ns_uri = None 15 | ns_alias = None 16 | 17 | def getExtensionArgs(self): 18 | """Get the string arguments that should be added to an OpenID 19 | message for this extension. 20 | 21 | @returns: A dictionary of completely non-namespaced arguments 22 | to be added. For example, if the extension's alias is 23 | 'uncle', and this method returns {'meat':'Hot Rats'}, the 24 | final message will contain {'openid.uncle.meat':'Hot Rats'} 25 | """ 26 | raise NotImplementedError 27 | 28 | def toMessage(self, message=None): 29 | """Add the arguments from this extension to the provided 30 | message, or create a new message containing only those 31 | arguments. 32 | 33 | @returns: The message with the extension arguments added 34 | """ 35 | if message is None: 36 | warnings.warn('Passing None to Extension.toMessage is deprecated. ' 37 | 'Creating a message assuming you want OpenID 2.', 38 | DeprecationWarning, stacklevel=2) 39 | message = message_module.Message(message_module.OPENID2_NS) 40 | 41 | implicit = message.isOpenID1() 42 | 43 | try: 44 | message.namespaces.addAlias(self.ns_uri, self.ns_alias, 45 | implicit=implicit) 46 | except KeyError: 47 | if message.namespaces.getAlias(self.ns_uri) != self.ns_alias: 48 | raise 49 | 50 | message.updateArgs(self.ns_uri, self.getExtensionArgs()) 51 | return message 52 | -------------------------------------------------------------------------------- /openid/yadis/parsehtml.py: -------------------------------------------------------------------------------- 1 | """Utilities to parse YADIS header from HTML.""" 2 | from __future__ import unicode_literals 3 | 4 | from lxml import etree 5 | 6 | from openid.yadis.constants import YADIS_HEADER_NAME 7 | 8 | __all__ = ['findHTMLMeta', 'MetaNotFound'] 9 | 10 | 11 | class MetaNotFound(Exception): 12 | """Yadis meta tag not found in the HTML page.""" 13 | 14 | 15 | def xpath_lower_case(context, values): 16 | """Return lower cased values in XPath.""" 17 | return [v.lower() for v in values] 18 | 19 | 20 | def findHTMLMeta(stream): 21 | """Look for a meta http-equiv tag with the YADIS header name. 22 | 23 | @param stream: Source of the html text 24 | @type stream: Readable text I/O file object 25 | 26 | @return: The URI from which to fetch the XRDS document 27 | @rtype: six.text_type 28 | 29 | @raises MetaNotFound: raised with the content that was 30 | searched as the first parameter. 31 | """ 32 | parser = etree.HTMLParser() 33 | try: 34 | html = etree.parse(stream, parser) 35 | except (ValueError, etree.XMLSyntaxError): 36 | raise MetaNotFound("Couldn't parse HTML page.") 37 | 38 | # Invalid input may return element with no content 39 | if html.getroot() is None: 40 | raise MetaNotFound("Couldn't parse HTML page.") 41 | 42 | # Create a XPath evaluator with a local function to lowercase values. 43 | xpath_evaluator = etree.XPathEvaluator(html, extensions={(None, 'lower-case'): xpath_lower_case}) 44 | # Find YADIS meta tag, case insensitive to the header name. 45 | yadis_headers = xpath_evaluator('/html/head/meta[lower-case(@http-equiv)="{}"]'.format(YADIS_HEADER_NAME.lower())) 46 | if not yadis_headers: 47 | raise MetaNotFound('Yadis meta tag not found.') 48 | 49 | yadis_header = yadis_headers[0] 50 | yadis_url = yadis_header.get('content') 51 | if yadis_url is None: 52 | raise MetaNotFound('Attribute "content" missing in yadis meta tag.') 53 | return yadis_url 54 | -------------------------------------------------------------------------------- /CHANGES-2.2.0: -------------------------------------------------------------------------------- 1 | 2 | * API changes 3 | * SQLStore implementations no longer create or use a 'settings' 4 | table 5 | * SRegResponse.fromSuccessResponse returns None when no signed 6 | arguments were found 7 | * Added functions to generate request/response HTML forms with 8 | auto-submission javascript 9 | * Consumer (relying party) API: AuthRequest.htmlMarkup 10 | * Server API: server.OpenIDResponse.toHTML 11 | * PAPE (Provider Authentication Policy Extension) module 12 | * Updated extension for specification draft 2 13 | * Request.fromSuccessResponse returns None if PAPE response 14 | arguments were not signed 15 | 16 | * New features 17 | * Demo server now supports OP-driven identifier selection 18 | * Demo consumer now has a "stateless" option 19 | * Fetchers now only read/request first megabyte of response 20 | 21 | * Bug fixes 22 | * NOT NULL constraints were added to SQLStore tables where 23 | appropriate 24 | * message.fromPostArgs: use query.items() instead of iteritems(), 25 | fixes #161 (Affects Django users) 26 | * check_authentication requests: copy entire response, not just 27 | signed fields. Fixes missing namespace in check_authentication 28 | requests 29 | * Consumer._verifyDiscoveryResults: fall back to OpenID 1.0 type if 30 | 1.1 endpoint cannot be found; fixes discovery verification bug for 31 | certain OpenID 1 identifiers 32 | * SQLStore._execSQL: convert unicode arguments to str to avoid 33 | postgresql api bug with unicode objects (Thanks to Marek Kuziel.) 34 | * MySQLStore: Use ENGINE instead of TYPE when creating tables 35 | * server.OpenIDResponse.toFormMarkup: Use return_to from the 36 | request, not the response fields (Not all responses (i.e. cancel, 37 | setup_needed) include a return_to field.) 38 | * server.AssociationRequest.answer: include session_type in 39 | no-encryption assoc responses 40 | * OpenIDServiceEndpoint.getDisplayIdentifier: Don't include the 41 | fragment in display identifiers. 42 | -------------------------------------------------------------------------------- /openid/yadis/services.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import six 4 | 5 | from openid.yadis.discover import DiscoveryFailure, discover 6 | from openid.yadis.etxrd import XRDSError, iterServices, parseXRDS 7 | from openid.yadis.filters import mkFilter 8 | 9 | 10 | def getServiceEndpoints(input_url, flt=None): 11 | """Perform the Yadis protocol on the input URL and return an 12 | iterable of resulting endpoint objects. 13 | 14 | @param flt: A filter object or something that is convertable to 15 | a filter object (using mkFilter) that will be used to generate 16 | endpoint objects. This defaults to generating BasicEndpoint 17 | objects. 18 | 19 | @param input_url: The URL on which to perform the Yadis protocol 20 | 21 | @return: The normalized identity URL and an iterable of endpoint 22 | objects generated by the filter function. 23 | 24 | @rtype: (six.text_type, [endpoint]) 25 | 26 | @raises DiscoveryFailure: when Yadis fails to obtain an XRDS document. 27 | """ 28 | result = discover(input_url) 29 | try: 30 | endpoints = applyFilter(result.normalized_uri, 31 | result.response_text, flt) 32 | except XRDSError as err: 33 | raise DiscoveryFailure(six.text_type(err), None) 34 | return (result.normalized_uri, endpoints) 35 | 36 | 37 | def applyFilter(normalized_uri, xrd_data, flt=None): 38 | """Generate an iterable of endpoint objects given this input data, 39 | presumably from the result of performing the Yadis protocol. 40 | 41 | @param normalized_uri: The input URL, after following redirects, 42 | as in the Yadis protocol. 43 | 44 | @param xrd_data: The XML text the XRDS file fetched from the 45 | normalized URI. 46 | @type xrd_data: six.binary_type 47 | """ 48 | flt = mkFilter(flt) 49 | et = parseXRDS(xrd_data) 50 | 51 | endpoints = [] 52 | for service_element in iterServices(et): 53 | endpoints.extend( 54 | flt.getServiceEndpoints(normalized_uri, service_element)) 55 | 56 | return endpoints 57 | -------------------------------------------------------------------------------- /examples/djopenid/README: -------------------------------------------------------------------------------- 1 | 2 | DJANGO EXAMPLE PACKAGE 3 | ====================== 4 | 5 | This package implements an example consumer and server for the Django 6 | Python web framework. You can get Django (and learn more about it) at 7 | 8 | http://www.djangoproject.com/ 9 | 10 | SETUP 11 | ===== 12 | 13 | 1. Install the OpenID library, version 2.0.0 or later. 14 | 15 | 2. Install Django. 16 | 17 | If you find that the examples doesn't run on newer versions of 18 | Django, please let us know! 19 | 20 | 3. Modify djopenid/settings.py appropriately; you may wish to change 21 | the database type or path, although the default settings should be 22 | sufficient for most systems. 23 | 24 | 4. In examples/djopenid/ run: 25 | 26 | python manage.py migrate 27 | 28 | 5. To run the example consumer or server, run 29 | 30 | python manage.py runserver [PORT] 31 | 32 | where PORT is the port number on which to listen. 33 | 34 | Note that if you want to try both the consumer and server at the 35 | same time, run the command twice with two different values for 36 | PORT. 37 | 38 | 6. Point your web browser at the server at 39 | 40 | http://localhost:PORT/ 41 | 42 | to begin. 43 | 44 | ABOUT THE CODE 45 | ============== 46 | 47 | The example server and consumer code provided in this package are 48 | intended to be instructional in the use of this OpenID library. While 49 | it is not recommended to use the example code in production, the code 50 | should be sufficient to explain the general use of the library. 51 | 52 | If you aren't familiar with the Django web framework, you can quickly 53 | start looking at the important code by looking in the 'views' modules: 54 | 55 | djopenid.consumer.views 56 | djopenid.server.views 57 | 58 | Each view is a python callable that responds to an HTTP request. 59 | Regardless of whether you use a framework, your application should 60 | look similar to these example applications. 61 | 62 | CONTACT 63 | ======= 64 | 65 | Please send bug reports, patches, and other feedback to 66 | 67 | http://openid.net/developers/dev-mailing-lists/ 68 | -------------------------------------------------------------------------------- /examples/djopenid/templates/server/trust.html: -------------------------------------------------------------------------------- 1 | {% extends "server/index.html" %} 2 | 3 | {% block body %} 4 | 5 | {% ifequal trust_root_valid "Valid" %} 6 | 7 |

The site {{ trust_root|escape }} has requested verification 8 | of your OpenID.

9 | 10 | {% include "server/pape_request_info.html" %} 11 | {% endifequal %} 12 | {% ifequal trust_root_valid "Invalid" %} 13 |
14 |

This request claims to be from {{ trust_root|escape }} but I have 15 | determined that it is a pack of lies. Beware, if you release 16 | information to them, they are likely to do unconscionable things with it, 17 | being the lying liars that they are.

18 |

Please tell the real {{ trust_root|escape }} that someone is 19 | trying to abuse your trust in their good name.

20 |
21 | {% endifequal %} 22 | {% ifequal trust_root_valid "Unreachable" %} 23 |

The site {{ trust_root|escape }} has requested verification 24 | of your OpenID. I have failed to reach it and thus cannot vouch for its 25 | authenticity. Perhaps it is on your local network.

26 | {% endifequal %} 27 | {% ifequal trust_root_valid "DISCOVERY_FAILED" %} 28 |

The site {{ trust_root|escape }} has requested verification 29 | of your OpenID. However, {{ trust_root|escape }} does not 30 | implement OpenID 2.0's relying party verification mechanism. Please use 31 | extra caution in deciding whether to release information to this party, 32 | and ask {{ trust_root|escape }} to implement relying party 33 | verification for your future transactions.

34 | 35 | {% include "server/pape_request_info.html" %} 36 | {% endifequal %} 37 | 38 | 39 | 40 |
41 |
43 | Verify your identity to the relying party? 44 | 45 |
46 | 47 | 48 |
49 |
50 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-openid2 # 2 | 3 | [![Build Status](https://travis-ci.org/ziima/python-openid.svg?branch=master)](https://travis-ci.org/ziima/python-openid) 4 | [![codecov](https://codecov.io/gh/ziima/python-openid/branch/master/graph/badge.svg)](https://codecov.io/gh/ziima/python-openid) 5 | [![PyPI](https://img.shields.io/pypi/v/python-openid2.svg)](https://pypi.org/pypi/python-openid2/) 6 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/python-openid2.svg)](https://pypi.org/pypi/python-openid2/) 7 | 8 | Python OpenID library - OpenID support for servers and consumers. 9 | 10 | This is a set of Python packages to support use of the OpenID decentralized identity system in your application. 11 | Want to enable single sign-on for your web site? 12 | Use the `openid.consumer package`. 13 | Want to run your own OpenID server? 14 | Check out `openid.server`. 15 | Includes example code and support for a variety of storage back-ends. 16 | 17 | ## REQUIREMENTS ## 18 | 19 | - Python 2.7, >=3.5 20 | - lxml 21 | - six 22 | - cryptography 23 | 24 | 25 | ## INSTALLATION ## 26 | 27 | To install the base library, just run the following command: 28 | 29 | pip install python-openid2 30 | 31 | 32 | ## GETTING STARTED ## 33 | 34 | The examples directory includes an example server and consumer 35 | implementation. See the README file in that directory for more 36 | information on running the examples. 37 | 38 | Library documentation is available in html form in the doc directory. 39 | 40 | 41 | ## LOGGING ## 42 | 43 | This library offers a logging hook that will record unexpected 44 | conditions that occur in library code. If a condition is recoverable, 45 | the library will recover and issue a log message. If it is not 46 | recoverable, the library will raise an exception. See the 47 | documentation for the openid.oidutil module for more on the logging 48 | hook. 49 | 50 | 51 | ## DOCUMENTATION ## 52 | 53 | The documentation in this library is in Epydoc format, which is 54 | detailed at: 55 | 56 | http://epydoc.sourceforge.net/ 57 | 58 | 59 | ## CONTACT ## 60 | 61 | Send bug reports, suggestions, comments, and questions to 62 | https://github.com/ziima/python-openid/issues/new 63 | 64 | If you have a bugfix or feature you'd like to contribute, don't 65 | hesitate to send it to us on GitHub. 66 | -------------------------------------------------------------------------------- /openid/cryptutil.py: -------------------------------------------------------------------------------- 1 | """Module containing a cryptographic-quality source of randomness and 2 | other cryptographically useful functionality 3 | 4 | Other configurations will need a quality source of random bytes and 5 | access to a function that will convert binary strings to long 6 | integers. 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | import codecs 11 | import warnings 12 | 13 | from openid.oidutil import fromBase64, toBase64 14 | 15 | __all__ = [ 16 | 'base64ToLong', 17 | 'binaryToLong', 18 | 'longToBase64', 19 | 'longToBinary', 20 | 'int_to_bytes', 21 | 'bytes_to_int', 22 | ] 23 | 24 | 25 | def bytes_to_int(value): 26 | """ 27 | Convert byte string to integer. 28 | 29 | @type value: six.binary_type 30 | @rtype: Union[six.integer_types] 31 | """ 32 | return int(codecs.encode(value, 'hex'), 16) 33 | 34 | 35 | def fix_btwoc(value): 36 | """ 37 | Utility function to ensure the output conforms the `btwoc` function output. 38 | 39 | See http://openid.net/specs/openid-authentication-2_0.html#btwoc for details. 40 | 41 | @type value: bytes or bytearray 42 | @rtype: bytes 43 | """ 44 | # Conversion to bytearray is python 2/3 compatible 45 | array = bytearray(value) 46 | # First bit must be zero. If it isn't, the bytes must be prepended by zero byte. 47 | if array[0] > 127: 48 | array = bytearray([0]) + array 49 | return bytes(array) 50 | 51 | 52 | def int_to_bytes(value): 53 | """ 54 | Convert integer to byte string. 55 | 56 | @type value: Union[six.integer_types] 57 | @rtype: six.binary_type 58 | """ 59 | hex_value = '{:x}'.format(value) 60 | if len(hex_value) % 2: 61 | hex_value = '0' + hex_value 62 | array = bytearray.fromhex(hex_value) 63 | # The output must be `btwoc` compatible 64 | return fix_btwoc(array) 65 | 66 | 67 | # Deprecated versions of bytes <--> int conversions 68 | def longToBinary(value): 69 | warnings.warn("Function longToBinary is deprecated in favor of int_to_bytes.", DeprecationWarning) 70 | return int_to_bytes(value) 71 | 72 | 73 | def binaryToLong(s): 74 | warnings.warn("Function binaryToLong is deprecated in favor of bytes_to_int.", DeprecationWarning) 75 | return bytes_to_int(s) 76 | 77 | 78 | def longToBase64(value): 79 | return toBase64(int_to_bytes(value)) 80 | 81 | 82 | def base64ToLong(s): 83 | return bytes_to_int(fromBase64(s)) 84 | -------------------------------------------------------------------------------- /admin/builddiscover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import unicode_literals 3 | 4 | import os.path 5 | 6 | from six.moves.urllib.parse import urljoin 7 | 8 | from openid.test import discoverdata 9 | 10 | manifest_header = """\ 11 | # This file contains test cases for doing YADIS identity URL and 12 | # service discovery. For each case, there are three URLs. The first 13 | # URL is the user input. The second is the identity URL and the third 14 | # is the URL from which the XRDS document should be read. 15 | # 16 | # The file format is as follows: 17 | # User URL Identity URL XRDS URL 18 | # 19 | # blank lines and lines starting with # should be ignored. 20 | # 21 | # To use this test: 22 | # 23 | # 1. Run your discovery routine on the User URL. 24 | # 25 | # 2. Compare the identity URL returned by the discovery routine to the 26 | # identity URL on that line of the file. It must be an EXACT match. 27 | # 28 | # 3. Do a regular HTTP GET on the XRDS URL. Compare the content that 29 | # was returned by your discovery routine with the content returned 30 | # from that URL. It should also be an exact match. 31 | 32 | """ 33 | 34 | 35 | def buildDiscover(base_url, out_dir): 36 | """Convert all files in a directory to apache mod_asis files in 37 | another directory.""" 38 | test_data = discoverdata.readTests(discoverdata.default_test_file) 39 | 40 | def writeTestFile(test_name): 41 | template = test_data[test_name] 42 | 43 | data = discoverdata.fillTemplate( 44 | test_name, template, base_url, discoverdata.example_xrds) 45 | 46 | out_file_name = os.path.join(out_dir, test_name) 47 | out_file = open(out_file_name, 'w') 48 | out_file.write(data) 49 | 50 | manifest = [manifest_header] 51 | for success, input_name, id_name, result_name in discoverdata.testlist: 52 | if not success: 53 | continue 54 | writeTestFile(input_name) 55 | 56 | input_url = urljoin(base_url, input_name) 57 | id_url = urljoin(base_url, id_name) 58 | result_url = urljoin(base_url, result_name) 59 | 60 | manifest.append('\t'.join((input_url, id_url, result_url))) 61 | manifest.append('\n') 62 | 63 | manifest_file_name = os.path.join(out_dir, 'manifest.txt') 64 | manifest_file = open(manifest_file_name, 'w') 65 | for chunk in manifest: 66 | manifest_file.write(chunk) 67 | manifest_file.close() 68 | 69 | 70 | if __name__ == '__main__': 71 | import sys 72 | buildDiscover(*sys.argv[1:]) 73 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/subsegments.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *nishitani 5 | 6 | 2007-12-25T11:33:39.000Z 7 | xri://= 8 | !E117.EF2F.454B.C707 9 | =!E117.EF2F.454B.C707 10 | 11 | http://openid.net/signon/1.0 12 | xri://!!1003!103 13 | https://linksafe.ezibroker.net/server/ 14 | 15 | 16 | xri://$res*auth*($v*2.0) 17 | xri://!!1003!103 18 | application/xrds+xml;trust=none 19 | http://resolve.ezibroker.net/resolve/=nishitani/ 20 | 21 | 22 | xri://+i-service*(+forwarding)*($v*1.0) 23 | 24 | xri://!!1003!103 25 | (+index) 26 | 27 | http://linksafe-forward.ezibroker.net/forwarding/ 28 | 29 | 30 | 31 | *masaki 32 | SUCCESS 33 | xri://!!1003 34 | !0000.0000.3B9A.CA01 35 | =!E117.EF2F.454B.C707!0000.0000.3B9A.CA01 36 | 37 | http://openid.net/signon/1.0 38 | xri://!!1003!103 39 | https://linksafe.ezibroker.net/server/ 40 | 41 | 42 | xri://+i-service*(+contact)*($v*1.0) 43 | 44 | xri://!!1003!103 45 | (+contact) 46 | 47 | http://linksafe-contact.ezibroker.net/contact/ 48 | 49 | 50 | xri://+i-service*(+forwarding)*($v*1.0) 51 | 52 | xri://!!1003!103 53 | (+index) 54 | 55 | http://linksafe-forward.ezibroker.net/forwarding/ 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | 5 | from setuptools import setup 6 | 7 | if 'sdist' in sys.argv: 8 | os.system('./admin/makedoc') 9 | 10 | # Import version from openid library itself 11 | VERSION = __import__('openid').__version__ 12 | INSTALL_REQUIRES = [ 13 | 'six', 14 | 'cryptography', 15 | 'lxml;platform_python_implementation=="CPython"', 16 | 'lxml <4.0;platform_python_implementation=="PyPy"', 17 | ] 18 | EXTRAS_REQUIRE = { 19 | 'quality': ('flake8', 'isort'), 20 | 'tests': ('mock', 'testfixtures', 'responses', 'coverage'), 21 | # Optional dependencies for fetchers 22 | 'httplib2': ('httplib2', ), 23 | 'pycurl': ('pycurl', ), 24 | 'requests': ('requests', ), 25 | # Dependencies for Django example 26 | 'djopenid': ('django<1.11.99', ), 27 | } 28 | LONG_DESCRIPTION = open('README.md').read() + '\n\n' + open('Changelog.md').read() 29 | CLASSIFIERS = [ 30 | 'Development Status :: 5 - Production/Stable', 31 | 'Environment :: Web Environment', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: Apache Software License', 34 | 'Operating System :: POSIX', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Programming Language :: Python :: 3.7', 42 | 'Programming Language :: Python :: 3.8', 43 | 'Topic :: Internet :: WWW/HTTP', 44 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries', 45 | 'Topic :: Software Development :: Libraries :: Python Modules', 46 | 'Topic :: System :: Systems Administration :: Authentication/Directory', 47 | ] 48 | 49 | 50 | setup( 51 | name='python-openid2', 52 | version=VERSION, 53 | description='Python OpenID library - OpenID support for servers and consumers.', 54 | long_description=LONG_DESCRIPTION, 55 | long_description_content_type='text/markdown', 56 | url='https://github.com/ziima/python-openid', 57 | packages=['openid', 58 | 'openid.consumer', 59 | 'openid.server', 60 | 'openid.store', 61 | 'openid.yadis', 62 | 'openid.extensions', 63 | 'openid.extensions.draft', 64 | ], 65 | python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', 66 | install_requires=INSTALL_REQUIRES, 67 | extras_require=EXTRAS_REQUIRE, 68 | # license specified by classifier. 69 | # license=getLicense(), 70 | author='Vlastimil Zíma', 71 | author_email='vlastimil.zima@gmail.com', 72 | classifiers=CLASSIFIERS, 73 | ) 74 | -------------------------------------------------------------------------------- /openid/test/test_trustroot.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | import unittest 5 | 6 | import six 7 | 8 | from openid.server.trustroot import TrustRoot 9 | 10 | with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'trustroot.txt'), 'rb') as test_data_file: 11 | trustroot_test_data = test_data_file.read().decode('utf-8') 12 | 13 | 14 | class ParseTest(unittest.TestCase): 15 | 16 | def test(self): 17 | ph, pdat, mh, mdat = parseTests(trustroot_test_data) 18 | 19 | for sanity, desc, case in getTests(['bad', 'insane', 'sane'], ph, pdat): 20 | tr = TrustRoot.parse(case) 21 | if sanity == 'sane': 22 | assert tr.isSane(), case 23 | elif sanity == 'insane': 24 | assert not tr.isSane(), case 25 | else: 26 | assert tr is None, tr 27 | 28 | @unittest.skipUnless(six.PY2, "Test for python 2 only") 29 | def test_double_port_py2(self): 30 | # Python 2 urlparse silently drops the ':90' port 31 | trust_root = TrustRoot.parse('http://*.example.com:80:90/') 32 | self.assertTrue(trust_root.isSane()) 33 | self.assertEqual(trust_root.buildDiscoveryURL(), 'http://www.example.com/') 34 | 35 | @unittest.skipUnless(six.PY3, "Test for python 3 only") 36 | def test_double_port_py3(self): 37 | # Python 3 urllib.parse complains about invalid port 38 | self.assertIsNone(TrustRoot.parse('http://*.example.com:80:90/')) 39 | 40 | 41 | class MatchTest(unittest.TestCase): 42 | 43 | def test(self): 44 | ph, pdat, mh, mdat = parseTests(trustroot_test_data) 45 | 46 | for expected_match, desc, line in getTests([1, 0], mh, mdat): 47 | tr, rt = line.split() 48 | tr = TrustRoot.parse(tr) 49 | self.assertIsNotNone(tr) 50 | 51 | match = tr.validateURL(rt) 52 | if expected_match: 53 | assert match 54 | else: 55 | assert not match 56 | 57 | 58 | def getTests(grps, head, dat): 59 | tests = [] 60 | top = head.strip() 61 | gdat = [i.strip() for i in dat.split('-' * 40 + '\n')] 62 | assert not gdat[0] 63 | assert len(gdat) == (len(grps) * 2 + 1), (gdat, grps) 64 | i = 1 65 | for x in grps: 66 | n, desc = gdat[i].split(': ') 67 | cases = gdat[i + 1].split('\n') 68 | assert len(cases) == int(n) 69 | for case in cases: 70 | tests.append((x, top + ' - ' + desc, case)) 71 | i += 2 72 | return tests 73 | 74 | 75 | def parseTests(data): 76 | parts = [i.strip() for i in data.split('=' * 40 + '\n')] 77 | assert not parts[0] 78 | _, ph, pdat, mh, mdat = parts 79 | return ph, pdat, mh, mdat 80 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | ## 3.2 ## 4 | * Add support for python 3.8. 5 | * Drop support for python 3.4. 6 | * Fix false positive redirect error in consumer verification. 7 | * Do not percent escape sub delimiters in path in URI normalization. Thanks Colin Watson for report. 8 | * Fix tests and static code checks. Thanks Colin Watson. 9 | 10 | ## 3.1 ## 11 | * Convert data values for extensions to text. 12 | * Fixes in Python 2/3 support. 13 | * Fix examples. 14 | * Add support for python 3.7 15 | * Fix static code checks 16 | * Use bumpversion 17 | 18 | ## 3.0 ## 19 | 20 | * Support Python3. 21 | * Change most of the API to the text strings. UTF-8 encoded byte string should be compatible. 22 | * Authentication methods based on SHA-256 are now preferred over SHA-1. 23 | * Use `cryptography` library for cryptography tasks. 24 | * Add new base64-based API for `DiffieHellman` class. 25 | * Refactor script to negotiate association with an OpenID server. 26 | * Decrease log levels on repetitive logs. 27 | * Default fetcher is picked from more options. 28 | * Remove `openid.consumer.html_parse` module. 29 | * Remove `hmacSha*`, `randomString`, `randrange` and `sha*` functions from `openid.cryptutil`. 30 | * A lot of refactoring and clean up. 31 | 32 | ### Deprecation ### 33 | * Binary strings are deprecated, unless explicitely allowed. 34 | * `hash_func` is deprecated in favor of `algorithm` in `DiffieHellmanSHA*ServerSession` and `DiffieHellmanSHA*ConsumerSession`. 35 | * `DiffieHellmanSHA*ServerSession.consumer_pubkey` is deprecated in favor of `consumer_public_key`. 36 | * Functions `longToBinary` and `binaryToLong` deprecated in favor of `int_to_bytes` and `bytes_to_int`, respectively. 37 | * Old `DiffieHellman` API is deprecated. 38 | 39 | ## 2.3.0 ## 40 | 41 | * Prevent timing attacks on signature comparison. Thanks to Carl Howells. 42 | * Prevent XXE attacks. 43 | * Fix unicode errors. Thanks to Kai Lautaportti. 44 | * Drop support for python versions < 2.7. 45 | * Use logging module. Thanks to Attila-Mihaly Balazs. 46 | * Allow signatory, encoder and decoder to be set for Server. Thanks to julio. 47 | * Fix URL limit to server responses. Thanks to Rodrigo Primo. 48 | * Fix several protocol errors. 49 | * Add utility method to AX store extension. 50 | * Fix curl detection. Thanks to Sergey Shepelev. 51 | * Use setuptools. Thanks to Tres Seaver. 52 | * Refactor `Message` class creation. 53 | * Add `RequestsFetcher`. Thanks to Lennonka. 54 | * Updated examples. 55 | * Add tox for testing. Thanks to Marc Abramowitz. 56 | * Refactor tests. 57 | * Clean code and add static checks. 58 | 59 | ### Deprecation ### 60 | * `Message.setOpenIDNamespace()` method. 61 | * `UndefinedOpenIDNamespace` exception. 62 | * `OpenIDRequest.namespace` attribute. 63 | * `openid.extensions.draft` packages, namely its `pape2` and `pape5` modules. 64 | -------------------------------------------------------------------------------- /openid/test/data/test1-discover.txt: -------------------------------------------------------------------------------- 1 | equiv 2 | Status: 200 OK 3 | Content-Type: text/html 4 | 5 | 6 | 7 | 8 | Joe Schmoe's Homepage 9 | 10 | 11 |

Joe Schmoe's Homepage

12 |

Blah blah blah blah blah blah blah

13 | 14 | 15 | 16 | header 17 | Status: 200 OK 18 | Content-Type: text/html 19 | YADIS_HEADER: URL_BASE/xrds 20 | 21 | 22 | 23 | Joe Schmoe's Homepage 24 | 25 | 26 |

Joe Schmoe's Homepage

27 |

Blah blah blah blah blah blah blah

28 | 29 | 30 | xrds 31 | Status: 200 OK 32 | Content-Type: application/xrds+xml 33 | 34 | 35 | 36 | xrds_ctparam 37 | Status: 200 OK 38 | Content-Type: application/xrds+xml; charset=UTF8 39 | 40 | 41 | 42 | xrds_ctcase 43 | Status: 200 OK 44 | Content-Type: appliCATION/XRDS+xml 45 | 46 | 47 | 48 | xrds_html 49 | Status: 200 OK 50 | Content-Type: text/html 51 | 52 | 53 | 54 | redir_equiv 55 | Status: 302 Found 56 | Content-Type: text/plain 57 | Location: URL_BASE/equiv 58 | 59 | You are presently being redirected. 60 | 61 | redir_header 62 | Status: 302 Found 63 | Content-Type: text/plain 64 | Location: URL_BASE/header 65 | 66 | You are presently being redirected. 67 | 68 | redir_xrds 69 | Status: 302 Found 70 | Content-Type: application/xrds+xml 71 | Location: URL_BASE/xrds 72 | 73 | 74 | 75 | redir_xrds_html 76 | Status: 302 Found 77 | Content-Type: text/plain 78 | Location: URL_BASE/xrds_html 79 | 80 | You are presently being redirected. 81 | 82 | redir_redir_equiv 83 | Status: 302 Found 84 | Content-Type: text/plain 85 | Location: URL_BASE/redir_equiv 86 | 87 | You are presently being redirected. 88 | 89 | lowercase_header 90 | Status: 200 OK 91 | Content-Type: text/html 92 | x-xrds-location: URL_BASE/xrds 93 | 94 | 95 | 96 | Joe Schmoe's Homepage 97 | 98 | 99 |

Joe Schmoe's Homepage

100 |

Blah blah blah blah blah blah blah

101 | 102 | 103 | 404_server_response 104 | Status: 404 Not Found 105 | 106 | EEk! 107 | 108 | 500_server_response 109 | Status: 500 Server error 110 | 111 | EEk! 112 | 113 | 201_server_response 114 | Status: 201 Created 115 | 116 | EEk! 117 | 118 | 404_with_header 119 | Status: 404 Not Found 120 | YADIS_HEADER: URL_BASE/xrds 121 | 122 | EEk! 123 | 124 | 404_with_meta 125 | Status: 404 Not Found 126 | Content-Type: text/html 127 | 128 | 129 | 130 | 131 | Joe Schmoe's Homepage 132 | 133 | 134 |

Joe Schmoe's Homepage

135 |

Blah blah blah blah blah blah blah

136 | 137 | 138 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | Python OpenID library example code 2 | ================================== 3 | 4 | The examples directory contains working code illustrating the use of 5 | the library for performing OpenID authentication, both as a consumer 6 | and a server. There are two kinds of examples, one that can run 7 | without any external dependencies, and one that uses the Django Web 8 | framework. The examples do not illustrate how to use all of the 9 | features of the library, but they should be a good starting point to 10 | see how to use this library with your code. 11 | 12 | Both the Django libraries and the BaseHTTPServer examples require that 13 | the OpenID library is installed or that it has been added to Python's 14 | search path (PYTHONPATH environment variable or sys.path). 15 | 16 | The Django example is probably a good place to start reading the 17 | code. There is little that is Django-specific about the OpenID logic 18 | in the example, and it should be easy to port to any framework. To run 19 | the django examples, see the README file in the djopenid subdirectory. 20 | 21 | The other examples use Python's built-in BaseHTTPServer and have a 22 | good deal of ad-hoc dispatching and rendering code mixed in 23 | 24 | Using the BaseHTTPServer examples 25 | ================================= 26 | 27 | This directory contains a working server and consumer that use this 28 | OpenID library. They are both written using python's standard 29 | BaseHTTPServer. 30 | 31 | 32 | To run the example system: 33 | 34 | 1. Make sure you've installed the library, as explained in the 35 | installation instructions. 36 | 37 | 2. Start the consumer server: 38 | 39 | python consumer.py --port 8001 40 | 41 | 42 | 3. In another terminal, start the identity server: 43 | 44 | python server.py --port 8000 45 | 46 | (Hit Ctrl-C in either server's window to stop that server.) 47 | 48 | 49 | 4. Open your web broswer, and go to the consumer server: 50 | 51 | http://localhost:8001/ 52 | 53 | Note that all pages the consumer server shows will have "Python OpenID 54 | Consumer Example" across the top. 55 | 56 | 57 | 5. Enter an identity url managed by the sample identity server: 58 | 59 | http://localhost:8000/id/bob 60 | 61 | 62 | 6. The browser will be redirected to the sample server, which will be 63 | requesting that you log in to proceed. Enter the username for the 64 | identity URL into the login box: 65 | 66 | bob 67 | 68 | Note that all pages the identity server shows will have "Python 69 | OpenID Server Example" across the top. 70 | 71 | 72 | 7. After you log in as bob, the server example will ask you if you 73 | want to allow http://localhost:8001/ to know your identity. Say 74 | yes. 75 | 76 | 77 | 8. You should end up back on the consumer site, at a page indicating 78 | you've logged in successfully. 79 | 80 | 81 | That's a basic OpenID login procedure. You can continue through it, 82 | playing with variations to see how they work. The python code is 83 | intended to be a straightforward example of how to use the python 84 | OpenID library to function as either an identity server or consumer. 85 | 86 | Getting help 87 | ============ 88 | 89 | Please send bug reports, patches, and other feedback to 90 | 91 | http://openid.net/developers/dev-mailing-lists/ 92 | -------------------------------------------------------------------------------- /openid/test/test_nonce.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import re 4 | import unittest 5 | 6 | import six 7 | 8 | from openid.store.nonce import checkTimestamp, make_nonce_salt, mkNonce, split as splitNonce 9 | 10 | nonce_re = re.compile(r'\A\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ') 11 | 12 | 13 | class TestMakeNonceSalt(unittest.TestCase): 14 | """Test `make_nonce_salt` function.""" 15 | 16 | def test_default(self): 17 | salt = make_nonce_salt() 18 | self.assertIsInstance(salt, six.text_type) 19 | self.assertEqual(len(salt), 6) 20 | 21 | def test_custom_length(self): 22 | salt = make_nonce_salt(32) 23 | self.assertIsInstance(salt, six.text_type) 24 | self.assertEqual(len(salt), 32) 25 | 26 | 27 | class NonceTest(unittest.TestCase): 28 | def test_mkNonce(self): 29 | nonce = mkNonce() 30 | self.assertIsNotNone(nonce_re.match(nonce)) 31 | self.assertEqual(len(nonce), 26) 32 | 33 | def test_mkNonce_when(self): 34 | nonce = mkNonce(0) 35 | self.assertIsNotNone(nonce_re.match(nonce)) 36 | self.assertTrue(nonce.startswith('1970-01-01T00:00:00Z')) 37 | self.assertEqual(len(nonce), 26) 38 | 39 | def test_splitNonce(self): 40 | s = '1970-01-01T00:00:00Z' 41 | expected_t = 0 42 | expected_salt = '' 43 | actual_t, actual_salt = splitNonce(s) 44 | self.assertEqual(actual_t, expected_t) 45 | self.assertEqual(actual_salt, expected_salt) 46 | 47 | def test_mkSplit(self): 48 | t = 42 49 | nonce_str = mkNonce(t) 50 | self.assertIsNotNone(nonce_re.match(nonce_str)) 51 | et, salt = splitNonce(nonce_str) 52 | self.assertEqual(len(salt), 6) 53 | self.assertEqual(et, t) 54 | 55 | 56 | class BadSplitTest(unittest.TestCase): 57 | cases = [ 58 | '', 59 | '1970-01-01T00:00:00+1:00', 60 | '1969-01-01T00:00:00Z', 61 | '1970-00-01T00:00:00Z', 62 | '1970.01-01T00:00:00Z', 63 | 'Thu Sep 7 13:29:31 PDT 2006', 64 | 'monkeys', 65 | ] 66 | 67 | def test(self): 68 | for nonce_str in self.cases: 69 | self.assertRaises(ValueError, splitNonce, nonce_str) 70 | 71 | 72 | class CheckTimestampTest(unittest.TestCase): 73 | cases = [ 74 | # exact, no allowed skew 75 | ('1970-01-01T00:00:00Z', 0, 0, True), 76 | 77 | # exact, large skew 78 | ('1970-01-01T00:00:00Z', 1000, 0, True), 79 | 80 | # no allowed skew, one second old 81 | ('1970-01-01T00:00:00Z', 0, 1, False), 82 | 83 | # many seconds old, outside of skew 84 | ('1970-01-01T00:00:00Z', 10, 50, False), 85 | 86 | # one second old, one second skew allowed 87 | ('1970-01-01T00:00:00Z', 1, 1, True), 88 | 89 | # One second in the future, one second skew allowed 90 | ('1970-01-01T00:00:02Z', 1, 1, True), 91 | 92 | # two seconds in the future, one second skew allowed 93 | ('1970-01-01T00:00:02Z', 1, 0, False), 94 | 95 | # malformed nonce string 96 | ('monkeys', 0, 0, False), 97 | ] 98 | 99 | def test(self): 100 | for nonce_string, allowed_skew, now, expected in self.cases: 101 | actual = checkTimestamp(nonce_string, allowed_skew, now) 102 | self.assertEqual(bool(actual), bool(expected)) 103 | -------------------------------------------------------------------------------- /examples/djopenid/templates/consumer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Django OpenID Example Consumer 4 | 29 | 30 | 31 | 32 |
33 | 34 |

35 | This is an example consumer built for the Django framework. Enter 36 | an OpenID in the box below. 37 |

38 | 39 | {% if error %} 40 |
{{ error|escape }}
41 | {% endif %} 42 | 43 | {% if url %} 44 |
45 | OpenID authentication succeeded; you authenticated as 46 | {{ url|escape }}. 47 | 48 |

49 | {% if sreg %} 50 | Simple Registration data returned: 51 | 52 |

    53 | {% for pair in sreg %} 54 |
  • {{ pair.0 }}: {{ pair.1 }}
  • 55 | {% endfor %} 56 |
57 | {% else %} 58 | The server returned no Simple Registration data. 59 | {% endif %} 60 | 61 | {% if ax %} 62 | Attribute Exchange data returned: 63 | 64 |
    65 | {% for pair in ax %} 66 |
  • {{ pair.0 }}: {{ pair.1|join:", " }}
  • 67 | {% endfor %} 68 |
69 | {% else %} 70 | The server returned no Attribute Exchange data. 71 | {% endif %} 72 | 73 | {% if pape %} 74 | An authentication policy response contained these policies: 75 | 76 |
    77 | {% for uri in pape.auth_policies %} 78 |
  • {{ uri }}
  • 79 | {% endfor %} 80 |
81 | {% else %} 82 | The server returned no authentication policy data (PAPE). 83 | {% endif %} 84 |

85 |
86 | {% endif %} 87 | 88 | {% if message %} 89 |
90 | {{ message|escape }} 91 |
92 | {% endif %} 93 | 94 | {% if failure_reason %} 95 |
96 | {{ failure_reason|escape }} 97 |
98 | {% endif %} 99 | 100 |
101 | 102 | 103 |

104 | Request these authentication policies 105 | (PAPE): 106 | 107 | 108 | {% for pair in pape_policies %} 109 | 110 | 111 | 116 | 117 | {% endfor %} 118 |
112 | 115 |
119 |

120 | 121 | 122 |
123 | 124 |
125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /openid/test/test_accept.py: -------------------------------------------------------------------------------- 1 | """Test `openid.yadis.accept` module.""" 2 | from __future__ import unicode_literals 3 | 4 | import os.path 5 | import unittest 6 | 7 | from openid.yadis import accept 8 | 9 | 10 | def getTestData(): 11 | """Read the test data off of disk 12 | 13 | () -> [(int, six.text_type)] 14 | """ 15 | filename = os.path.join(os.path.dirname(__file__), 'data', 'accept.txt') 16 | with open(filename, 'rb') as data_file: 17 | content = data_file.read().decode('utf-8') 18 | lines = enumerate(content.splitlines(), start=1) 19 | return lines 20 | 21 | 22 | def chunk(lines): 23 | """Return groups of lines separated by whitespace or comments 24 | 25 | [(int, six.text_type)] -> [[(int, six.text_type)]] 26 | """ 27 | chunks = [] 28 | chunk = [] 29 | for lineno, line in lines: 30 | stripped = line.strip() 31 | if not stripped or stripped[0] == '#': 32 | if chunk: 33 | chunks.append(chunk) 34 | chunk = [] 35 | else: 36 | chunk.append((lineno, stripped)) 37 | 38 | if chunk: 39 | chunks.append(chunk) 40 | 41 | return chunks 42 | 43 | 44 | def parseLines(chunk): 45 | """Take the given chunk of lines and turn it into a test data dictionary 46 | 47 | [(int, six.text_type)] -> {six.text_type:(int, six.text_type)} 48 | """ 49 | items = {} 50 | for (lineno, line) in chunk: 51 | header, data = line.split(':', 1) 52 | header = header.lower() 53 | items[header] = (lineno, data.strip()) 54 | 55 | return items 56 | 57 | 58 | def parseAvailable(available_text): 59 | """Parse an Available: line's data 60 | 61 | six.text_type -> [six.text_type] 62 | """ 63 | return [s.strip() for s in available_text.split(',')] 64 | 65 | 66 | def parseExpected(expected_text): 67 | """Parse an Expected: line's data 68 | 69 | six.text_type -> [(six.text_type, float)] 70 | """ 71 | expected = [] 72 | if expected_text: 73 | for chunk in expected_text.split(','): 74 | chunk = chunk.strip() 75 | mtype, qstuff = chunk.split(';') 76 | mtype = mtype.strip() 77 | assert '/' in mtype 78 | qstuff = qstuff.strip() 79 | q, qstr = qstuff.split('=') 80 | assert q == 'q' 81 | qval = float(qstr) 82 | expected.append((mtype, qval)) 83 | 84 | return expected 85 | 86 | 87 | class MatchAcceptTest(unittest.TestCase): 88 | 89 | def runTest(self): 90 | lines = getTestData() 91 | chunks = chunk(lines) 92 | data_sets = [parseLines(line) for line in chunks] 93 | for data in data_sets: 94 | lnos = [] 95 | lno, accept_header = data['accept'] 96 | lnos.append(lno) 97 | lno, avail_data = data['available'] 98 | lnos.append(lno) 99 | try: 100 | available = parseAvailable(avail_data) 101 | except Exception: 102 | print('On line', lno) 103 | raise 104 | 105 | lno, exp_data = data['expected'] 106 | lnos.append(lno) 107 | try: 108 | expected = parseExpected(exp_data) 109 | except Exception: 110 | print('On line', lno) 111 | raise 112 | 113 | accepted = accept.parseAcceptHeader(accept_header) 114 | actual = accept.matchTypes(accepted, available) 115 | self.assertEqual(actual, expected) 116 | -------------------------------------------------------------------------------- /openid/test/data/accept.txt: -------------------------------------------------------------------------------- 1 | # Accept: [Accept: header value from RFC2616, 2 | # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html] 3 | # Available: [whitespace-separated content types] 4 | # Expected: [Accept-header like list, containing the available content 5 | # types with their q-values] 6 | 7 | Accept: */* 8 | Available: text/plain 9 | Expected: text/plain; q=1.0 10 | 11 | Accept: */* 12 | Available: text/plain, text/html 13 | Expected: text/plain; q=1.0, text/html; q=1.0 14 | 15 | # The order matters 16 | Accept: */* 17 | Available: text/html, text/plain 18 | Expected: text/html; q=1.0, text/plain; q=1.0 19 | 20 | Accept: text/*, */*; q=0.9 21 | Available: text/plain, image/jpeg 22 | Expected: text/plain; q=1.0, image/jpeg; q=0.9 23 | 24 | Accept: text/*, */*; q=0.9 25 | Available: image/jpeg, text/plain 26 | Expected: text/plain; q=1.0, image/jpeg; q=0.9 27 | 28 | # wildcard subtypes still reject differing main types 29 | Accept: text/* 30 | Available: image/jpeg, text/plain 31 | Expected: text/plain; q=1.0 32 | 33 | Accept: text/html 34 | Available: text/html 35 | Expected: text/html; q=1.0 36 | 37 | Accept: text/html, text/* 38 | Available: text/html 39 | Expected: text/html; q=1.0 40 | 41 | Accept: text/html, text/* 42 | Available: text/plain, text/html 43 | Expected: text/plain; q=1.0, text/html; q=1.0 44 | 45 | Accept: text/html, text/*; q=0.9 46 | Available: text/plain, text/html 47 | Expected: text/html; q=1.0, text/plain; q=0.9 48 | 49 | # If a more specific type has a higher q-value, then the higher value wins 50 | Accept: text/*; q=0.9, text/html 51 | Available: text/plain, text/html 52 | Expected: text/html; q=1.0, text/plain; q=0.9 53 | 54 | Accept: */*, text/*; q=0.9, text/html; q=0.1 55 | Available: text/plain, text/html, image/monkeys 56 | Expected: image/monkeys; q=1.0, text/plain; q=0.9, text/html; q=0.1 57 | 58 | Accept: text/*, text/html; q=0 59 | Available: text/html 60 | Expected: 61 | 62 | Accept: text/*, text/html; q=0 63 | Available: text/html, text/plain 64 | Expected: text/plain; q=1.0 65 | 66 | Accept: text/html 67 | Available: text/plain 68 | Expected: 69 | 70 | Accept: application/xrds+xml, text/html; q=0.9 71 | Available: application/xrds+xml, text/html 72 | Expected: application/xrds+xml; q=1.0, text/html; q=0.9 73 | 74 | Accept: application/xrds+xml, */*; q=0.9 75 | Available: application/xrds+xml, text/html 76 | Expected: application/xrds+xml; q=1.0, text/html; q=0.9 77 | 78 | Accept: application/xrds+xml, application/xhtml+xml; q=0.9, text/html; q=0.8, text/xml; q=0.7 79 | Available: application/xrds+xml, text/html 80 | Expected: application/xrds+xml; q=1.0, text/html; q=0.8 81 | 82 | # See http://www.rfc-editor.org/rfc/rfc3023.txt, section A.13 83 | Accept: application/xrds 84 | Available: application/xrds+xml 85 | Expected: 86 | 87 | Accept: application/xrds+xml 88 | Available: application/xrds 89 | Expected: 90 | 91 | Accept: application/xml 92 | Available: application/xrds+xml 93 | Expected: 94 | 95 | Available: application/xrds+xml 96 | Accept: application/xml 97 | Expected: 98 | 99 | 100 | 101 | ################################################# 102 | # The tests below this line are documentation of how this library 103 | # works. If the implementation changes, it's acceptable to change the 104 | # test to reflect that. These are specified so that we can make sure 105 | # that the current implementation actually works the way that we 106 | # expect it to given these inputs. 107 | 108 | Accept: text/html;level=1 109 | Available: text/html 110 | Expected: text/html; q=1.0 111 | 112 | Accept: text/html; level=1, text/html; level=9; q=0.1 113 | Available: text/html 114 | Expected: text/html; q=1.0 115 | 116 | Accept: text/html; level=9; q=0.1, text/html; level=1 117 | Available: text/html 118 | Expected: text/html; q=1.0 119 | -------------------------------------------------------------------------------- /openid/test/test_xri.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from unittest import TestCase 4 | 5 | from openid.yadis import xri 6 | 7 | 8 | class XriDiscoveryTestCase(TestCase): 9 | def test_isXRI(self): 10 | i = xri.identifierScheme 11 | self.assertEqual(i('=john.smith'), 'XRI') 12 | self.assertEqual(i('@smiths/john'), 'XRI') 13 | self.assertEqual(i('smoker.myopenid.com'), 'URI') 14 | self.assertEqual(i('xri://=john'), 'XRI') 15 | self.assertEqual(i(''), 'URI') 16 | 17 | 18 | class XriEscapingTestCase(TestCase): 19 | def test_escaping_percents(self): 20 | self.assertEqual(xri.escapeForIRI('@example/abc%2Fd/ef'), '@example/abc%252Fd/ef') 21 | 22 | def test_escaping_xref(self): 23 | # no escapes 24 | esc = xri.escapeForIRI 25 | self.assertEqual('@example/foo/(@bar)', esc('@example/foo/(@bar)')) 26 | # escape slashes 27 | self.assertEqual('@example/foo/(@bar%2Fbaz)', esc('@example/foo/(@bar/baz)')) 28 | self.assertEqual('@example/foo/(@bar%2Fbaz)/(+a%2Fb)', esc('@example/foo/(@bar/baz)/(+a/b)')) 29 | # escape query ? and fragment # 30 | self.assertEqual('@example/foo/(@baz%3Fp=q%23r)?i=j#k', esc('@example/foo/(@baz?p=q#r)?i=j#k')) 31 | 32 | 33 | class XriTransformationTestCase(TestCase): 34 | def test_to_iri_normal(self): 35 | self.assertEqual(xri.toIRINormal('@example'), 'xri://@example') 36 | 37 | def test_iri_to_url(self): 38 | s = u'l\xa1m\U00101010n' 39 | expected = 'l%C2%A1m%F4%81%80%90n' 40 | self.assertEqual(xri.iriToURI(s), expected) 41 | 42 | 43 | class CanonicalIDTest(TestCase): 44 | def mkTest(providerID, canonicalID, isAuthoritative): 45 | def test(self): 46 | result = xri.providerIsAuthoritative(providerID, canonicalID) 47 | format = "%s providing %s, expected %s" 48 | message = format % (providerID, canonicalID, isAuthoritative) 49 | self.assertEqual(result, isAuthoritative, message) 50 | 51 | return test 52 | 53 | test_equals = mkTest('=', '=!698.74D1.A1F2.86C7', True) 54 | test_atOne = mkTest('@!1234', '@!1234!ABCD', True) 55 | test_atTwo = mkTest('@!1234!5678', '@!1234!5678!ABCD', True) 56 | 57 | test_atEqualsFails = mkTest('@!1234', '=!1234!ABCD', False) 58 | test_tooDeepFails = mkTest('@!1234', '@!1234!ABCD!9765', False) 59 | test_atEqualsAndTooDeepFails = mkTest('@!1234!ABCD', '=!1234', False) 60 | test_differentBeginningFails = mkTest('=!BABE', '=!D00D', False) 61 | 62 | 63 | class TestGetRootAuthority(TestCase): 64 | def mkTest(the_xri, expected_root): 65 | def test(self): 66 | actual_root = xri.rootAuthority(the_xri) 67 | self.assertEqual(actual_root, xri.XRI(expected_root)) 68 | return test 69 | 70 | test_at = mkTest("@foo", "@") 71 | test_atStar = mkTest("@foo*bar", "@") 72 | test_atStarStar = mkTest("@*foo*bar", "@") 73 | test_atWithPath = mkTest("@foo/bar", "@") 74 | test_bangBang = mkTest("!!990!991", "!") 75 | test_bang = mkTest("!1001!02", "!") 76 | test_equalsStar = mkTest("=foo*bar", "=") 77 | test_xrefPath = mkTest("(example.com)/foo", "(example.com)") 78 | test_xrefStar = mkTest("(example.com)*bar/foo", "(example.com)") 79 | test_uriAuth = mkTest("baz.example.com/foo", "baz.example.com") 80 | test_uriAuthPort = mkTest("baz.example.com:8080/foo", 81 | "baz.example.com:8080") 82 | 83 | # Looking at the ABNF in XRI Syntax 2.0, I don't think you can 84 | # have example.com*bar. You can do (example.com)*bar, but that 85 | # would mean something else. 86 | # ("example.com*bar/(=baz)", "example.com*bar"), 87 | # ("baz.example.com!01/foo", "baz.example.com!01"), 88 | 89 | 90 | if __name__ == '__main__': 91 | import unittest 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /examples/djopenid/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility code for the Django example consumer and server. 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | import six 7 | from django.conf import settings 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.db import connection 10 | from django.shortcuts import render 11 | from openid.store import sqlstore 12 | from openid.store.filestore import FileOpenIDStore 13 | from openid.yadis.constants import YADIS_CONTENT_TYPE 14 | 15 | 16 | def getOpenIDStore(filestore_path, table_prefix): 17 | """ 18 | Returns an OpenID association store object based on the database 19 | engine chosen for this Django application. 20 | 21 | * If no database engine is chosen, a filesystem-based store will 22 | be used whose path is filestore_path. 23 | 24 | * If a database engine is chosen, a store object for that database 25 | type will be returned. 26 | 27 | * If the chosen engine is not supported by the OpenID library, 28 | raise ImproperlyConfigured. 29 | 30 | * If a database store is used, this will create the tables 31 | necessary to use it. The table names will be prefixed with 32 | table_prefix. DO NOT use the same table prefix for both an 33 | OpenID consumer and an OpenID server in the same database. 34 | 35 | The result of this function should be passed to the Consumer 36 | constructor as the store parameter. 37 | """ 38 | if not settings.DATABASES.get('default', {'ENGINE': None}).get('ENGINE'): 39 | return FileOpenIDStore(filestore_path) 40 | 41 | # Possible side-effect: create a database connection if one isn't 42 | # already open. 43 | connection.cursor() 44 | 45 | # Create table names to specify for SQL-backed stores. 46 | tablenames = { 47 | 'associations_table': table_prefix + 'openid_associations', 48 | 'nonces_table': table_prefix + 'openid_nonces', 49 | } 50 | 51 | types = { 52 | 'django.db.backends.postgresql': sqlstore.PostgreSQLStore, 53 | 'django.db.backends.mysql': sqlstore.MySQLStore, 54 | 'django.db.backends.sqlite3': sqlstore.SQLiteStore, 55 | } 56 | 57 | engine = settings.DATABASES.get('default', {'ENGINE': None}).get('ENGINE') 58 | try: 59 | s = types[engine](connection.connection, **tablenames) 60 | except KeyError: 61 | raise ImproperlyConfigured("Database engine %s not supported by OpenID library" % engine) 62 | 63 | try: 64 | s.createTables() 65 | except Exception: 66 | # XXX This is not the Right Way to do this, but because the 67 | # underlying database implementation might differ in behavior 68 | # at this point, we can't reliably catch the right 69 | # exception(s) here. Ideally, the SQL store in the OpenID 70 | # library would catch exceptions that it expects and fail 71 | # silently, but that could be bad, too. More ideally, the SQL 72 | # store would not attempt to create tables it knows already 73 | # exists. 74 | pass 75 | 76 | return s 77 | 78 | 79 | def normalDict(request_data): 80 | """ 81 | Converts a django request MutliValueDict (e.g., request.GET, 82 | request.POST) into a standard python dict whose values are the 83 | first value from each of the MultiValueDict's value lists. This 84 | avoids the OpenID library's refusal to deal with dicts whose 85 | values are lists, because in OpenID, each key in the query arg set 86 | can have at most one value. 87 | """ 88 | return dict((k, v) for k, v in six.iteritems(request_data)) 89 | 90 | 91 | def renderXRDS(request, type_uris, endpoint_urls): 92 | """Render an XRDS page with the specified type URIs and endpoint 93 | URLs in one service block, and return a response with the 94 | appropriate content-type. 95 | """ 96 | context = {'type_uris': type_uris, 'endpoint_urls': endpoint_urls} 97 | return render(request, 'xrds.xml', context, content_type=YADIS_CONTENT_TYPE) 98 | -------------------------------------------------------------------------------- /openid/store/nonce.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import itertools 4 | import random 5 | import string 6 | from calendar import timegm 7 | from time import gmtime, strftime, strptime, time 8 | 9 | from openid.oidutil import string_to_text 10 | 11 | __all__ = [ 12 | 'split', 13 | 'mkNonce', 14 | 'checkTimestamp', 15 | ] 16 | 17 | 18 | NONCE_CHARS = string.ascii_letters + string.digits 19 | 20 | # Keep nonces for five hours (allow five hours for the combination of 21 | # request time and clock skew). This is probably way more than is 22 | # necessary, but there is not much overhead in storing nonces. 23 | SKEW = 60 * 60 * 5 24 | 25 | time_fmt = '%Y-%m-%dT%H:%M:%SZ' 26 | time_str_len = len('0000-00-00T00:00:00Z') 27 | 28 | 29 | def split(nonce_string): 30 | """Extract a timestamp from the given nonce string 31 | 32 | @param nonce_string: the nonce from which to extract the timestamp 33 | @type nonce_string: six.text_type, six.binary_type is deprecated 34 | 35 | @returns: A pair of a Unix timestamp and the salt characters 36 | @returntype: (int, six.text_type) 37 | 38 | @raises ValueError: if the nonce does not start with a correctly 39 | formatted time string 40 | """ 41 | nonce_string = string_to_text(nonce_string, 42 | "Binary values for nonce_string are deprecated. Use text input instead.") 43 | 44 | timestamp_str = nonce_string[:time_str_len] 45 | timestamp = timegm(strptime(timestamp_str, time_fmt)) 46 | if timestamp < 0: 47 | raise ValueError('time out of range') 48 | return timestamp, nonce_string[time_str_len:] 49 | 50 | 51 | def checkTimestamp(nonce_string, allowed_skew=SKEW, now=None): 52 | """Is the timestamp that is part of the specified nonce string 53 | within the allowed clock-skew of the current time? 54 | 55 | @param nonce_string: The nonce that is being checked 56 | @type nonce_string: six.text_type, six.binary_type is deprecated 57 | 58 | @param allowed_skew: How many seconds should be allowed for 59 | completing the request, allowing for clock skew. 60 | @type allowed_skew: int 61 | 62 | @param now: The current time, as a Unix timestamp 63 | @type now: int 64 | 65 | @returntype: bool 66 | @returns: Whether the timestamp is correctly formatted and within 67 | the allowed skew of the current time. 68 | """ 69 | try: 70 | stamp, _ = split(nonce_string) 71 | except ValueError: 72 | return False 73 | else: 74 | if now is None: 75 | now = time() 76 | 77 | # Time after which we should not use the nonce 78 | past = now - allowed_skew 79 | 80 | # Time that is too far in the future for us to allow 81 | future = now + allowed_skew 82 | 83 | # the stamp is not too far in the future and is not too far in 84 | # the past 85 | return past <= stamp <= future 86 | 87 | 88 | def make_nonce_salt(length=6): 89 | """ 90 | Generate and return a nonce salt. 91 | 92 | @param length: Length of the generated string. 93 | @type length: int 94 | @rtype: six.text_type 95 | """ 96 | sys_random = random.SystemRandom() 97 | random_chars = itertools.starmap(sys_random.choice, itertools.repeat((NONCE_CHARS, ), length)) 98 | return ''.join(random_chars) 99 | 100 | 101 | def mkNonce(when=None): 102 | """Generate a nonce with the current timestamp 103 | 104 | @param when: Unix timestamp representing the issue time of the 105 | nonce. Defaults to the current time. 106 | @type when: int 107 | 108 | @returntype: six.text_type 109 | @returns: A string that should be usable as a one-way nonce 110 | 111 | @see: time 112 | """ 113 | if when is None: 114 | t = gmtime() 115 | else: 116 | t = gmtime(when) 117 | 118 | time_str = strftime(time_fmt, t) 119 | return time_str + make_nonce_salt() 120 | -------------------------------------------------------------------------------- /openid/test/test_cryptutil.py: -------------------------------------------------------------------------------- 1 | """Test `openid.cryptutil` module.""" 2 | from __future__ import unicode_literals 3 | 4 | import os.path 5 | import random 6 | import sys 7 | import unittest 8 | import warnings 9 | 10 | import six 11 | 12 | from openid import cryptutil 13 | 14 | 15 | # Most of the purpose of this test is to make sure that cryptutil can 16 | # find a good source of randomness on this machine. 17 | class TestLongBinary(unittest.TestCase): 18 | """Test `longToBinary` and `binaryToLong` functions.""" 19 | 20 | def test_binaryLongConvert(self): 21 | MAX = sys.maxsize 22 | with warnings.catch_warnings(): 23 | warnings.simplefilter('ignore', category=DeprecationWarning) 24 | for iteration in range(500): 25 | n = 0 26 | for i in range(10): 27 | n += random.randrange(MAX) 28 | 29 | s = cryptutil.longToBinary(n) 30 | assert isinstance(s, six.binary_type) 31 | n_prime = cryptutil.binaryToLong(s) 32 | assert n == n_prime, (n, n_prime) 33 | 34 | cases = [ 35 | (b'\x00', 0), 36 | (b'\x01', 1), 37 | (b'\x7F', 127), 38 | (b'\x00\xFF', 255), 39 | (b'\x00\x80', 128), 40 | (b'\x00\x81', 129), 41 | (b'\x00\x80\x00', 32768), 42 | (b'OpenID is cool', 1611215304203901150134421257416556) 43 | ] 44 | 45 | with warnings.catch_warnings(): 46 | warnings.simplefilter('ignore', category=DeprecationWarning) 47 | for s, n in cases: 48 | n_prime = cryptutil.binaryToLong(s) 49 | s_prime = cryptutil.longToBinary(n) 50 | assert n == n_prime, (s, n, n_prime) 51 | assert s == s_prime, (n, s, s_prime) 52 | 53 | 54 | class TestFixBtwoc(unittest.TestCase): 55 | """Test `fix_btwoc` function.""" 56 | 57 | cases = ( 58 | (b'\x00', b'\x00'), 59 | (b'\x01', b'\x01'), 60 | (b'\x7F', b'\x7F'), 61 | (b'\x80', b'\x00\x80'), 62 | (b'\xFF', b'\x00\xFF'), 63 | ) 64 | 65 | def test_bytes(self): 66 | for value, output in self.cases: 67 | self.assertEqual(cryptutil.fix_btwoc(value), output) 68 | 69 | def test_bytearray(self): 70 | for value, output in self.cases: 71 | self.assertEqual(cryptutil.fix_btwoc(bytearray(value)), output) 72 | 73 | 74 | class TestBytesIntConversion(unittest.TestCase): 75 | """Test bytes <-> int conversions.""" 76 | 77 | # Examples from http://openid.net/specs/openid-authentication-2_0.html#btwoc 78 | cases = [ 79 | (b'\x00', 0), 80 | (b'\x01', 1), 81 | (b'\x7F', 127), 82 | (b'\x00\xFF', 255), 83 | (b'\x00\x80', 128), 84 | (b'\x00\x81', 129), 85 | (b'\x00\x80\x00', 32768), 86 | (b'OpenID is cool', 1611215304203901150134421257416556) 87 | ] 88 | 89 | def test_conversions(self): 90 | for string, number in self.cases: 91 | self.assertEqual(cryptutil.bytes_to_int(string), number) 92 | self.assertEqual(cryptutil.int_to_bytes(number), string) 93 | 94 | 95 | class TestLongToBase64(unittest.TestCase): 96 | """Test `longToBase64` function.""" 97 | 98 | def test_longToBase64(self): 99 | f = open(os.path.join(os.path.dirname(__file__), 'n2b64')) 100 | try: 101 | for line in f: 102 | parts = line.strip().split(' ') 103 | assert parts[0] == cryptutil.longToBase64(int(parts[1])) 104 | finally: 105 | f.close() 106 | 107 | 108 | class TestBase64ToLong(unittest.TestCase): 109 | """Test `Base64ToLong` function.""" 110 | 111 | def test_base64ToLong(self): 112 | f = open(os.path.join(os.path.dirname(__file__), 'n2b64')) 113 | try: 114 | for line in f: 115 | parts = line.strip().split(' ') 116 | assert int(parts[1]) == cryptutil.base64ToLong(parts[0]) 117 | finally: 118 | f.close() 119 | -------------------------------------------------------------------------------- /examples/djopenid/server/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import django 4 | from django.http import HttpRequest 5 | from django.test.testcases import TestCase 6 | from django.urls import reverse 7 | from openid.message import Message 8 | from openid.server.server import HTTP_REDIRECT, CheckIDRequest 9 | from openid.yadis.constants import YADIS_CONTENT_TYPE 10 | from openid.yadis.services import applyFilter 11 | from six.moves.urllib.parse import urljoin 12 | 13 | from .. import util 14 | from ..server import views 15 | 16 | # Allow django tests to run through discover 17 | django.setup() 18 | 19 | 20 | def dummyRequest(): 21 | request = HttpRequest() 22 | request.session = {} 23 | request.META['HTTP_HOST'] = 'example.cz' 24 | request.META['SERVER_PROTOCOL'] = 'HTTP' 25 | return request 26 | 27 | 28 | class TestProcessTrustResult(TestCase): 29 | def setUp(self): 30 | self.request = dummyRequest() 31 | 32 | id_url = urljoin('http://example.cz/', reverse('server:local_id')) 33 | 34 | # Set up the OpenID request we're responding to. 35 | op_endpoint = 'http://127.0.0.1:8080/endpoint' 36 | message = Message.fromPostArgs({ 37 | 'openid.mode': 'checkid_setup', 38 | 'openid.identity': id_url, 39 | 'openid.return_to': 'http://127.0.0.1/%s' % (self.id(),), 40 | 'openid.sreg.required': 'postcode', 41 | }) 42 | self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint) 43 | 44 | views.setRequest(self.request, self.openid_request) 45 | 46 | def test_allow(self): 47 | self.request.POST['allow'] = 'Yes' 48 | 49 | response = views.processTrustResult(self.request) 50 | 51 | self.assertEqual(response.status_code, HTTP_REDIRECT) 52 | finalURL = response['location'] 53 | self.assertIn('openid.mode=id_res', finalURL) 54 | self.assertIn('openid.identity=', finalURL) 55 | self.assertIn('openid.sreg.postcode=12345', finalURL) 56 | 57 | def test_cancel(self): 58 | self.request.POST['cancel'] = 'Yes' 59 | 60 | response = views.processTrustResult(self.request) 61 | 62 | self.assertEqual(response.status_code, HTTP_REDIRECT) 63 | finalURL = response['location'] 64 | self.assertIn('openid.mode=cancel', finalURL) 65 | self.assertNotIn('openid.identity=', finalURL) 66 | self.assertNotIn('openid.sreg.postcode=12345', finalURL) 67 | 68 | 69 | class TestShowDecidePage(TestCase): 70 | def test_unreachableRealm(self): 71 | self.request = dummyRequest() 72 | 73 | id_url = urljoin('http://example.cz/', reverse('server:local_id')) 74 | 75 | # Set up the OpenID request we're responding to. 76 | op_endpoint = 'http://127.0.0.1:8080/endpoint' 77 | message = Message.fromPostArgs({ 78 | 'openid.mode': 'checkid_setup', 79 | 'openid.identity': id_url, 80 | 'openid.return_to': 'http://unreachable.invalid/%s' % (self.id(),), 81 | 'openid.sreg.required': 'postcode', 82 | }) 83 | self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint) 84 | 85 | views.setRequest(self.request, self.openid_request) 86 | 87 | response = views.showDecidePage(self.request, self.openid_request) 88 | self.assertContains(response, 'trust_root_valid is Unreachable') 89 | 90 | 91 | class TestGenericXRDS(TestCase): 92 | def test_genericRender(self): 93 | """Render an XRDS document with a single type URI and a single endpoint URL 94 | Parse it to see that it matches.""" 95 | request = dummyRequest() 96 | 97 | type_uris = ['A_TYPE'] 98 | endpoint_url = 'A_URL' 99 | response = util.renderXRDS(request, type_uris, [endpoint_url]) 100 | 101 | requested_url = 'http://requested.invalid/' 102 | (endpoint,) = applyFilter(requested_url, response.content) 103 | 104 | self.assertEqual(response['Content-Type'], YADIS_CONTENT_TYPE) 105 | self.assertEqual(endpoint.type_uris, type_uris) 106 | self.assertEqual(endpoint.uri, endpoint_url) 107 | -------------------------------------------------------------------------------- /openid/store/memstore.py: -------------------------------------------------------------------------------- 1 | """A simple store using only in-process memory.""" 2 | from __future__ import unicode_literals 3 | 4 | import copy 5 | import time 6 | 7 | import six 8 | 9 | from openid.store import nonce 10 | 11 | 12 | class ServerAssocs(object): 13 | def __init__(self): 14 | self.assocs = {} 15 | 16 | def set(self, assoc): 17 | self.assocs[assoc.handle] = assoc 18 | 19 | def get(self, handle): 20 | return self.assocs.get(handle) 21 | 22 | def remove(self, handle): 23 | try: 24 | del self.assocs[handle] 25 | except KeyError: 26 | return False 27 | else: 28 | return True 29 | 30 | def best(self): 31 | """Returns association with the oldest issued date. 32 | 33 | or None if there are no associations. 34 | """ 35 | best = None 36 | for assoc in self.assocs.values(): 37 | if best is None or best.issued < assoc.issued: 38 | best = assoc 39 | return best 40 | 41 | def cleanup(self): 42 | """Remove expired associations. 43 | 44 | @return: tuple of (removed associations, remaining associations) 45 | """ 46 | remove = [] 47 | for handle, assoc in six.iteritems(self.assocs): 48 | if assoc.getExpiresIn() == 0: 49 | remove.append(handle) 50 | for handle in remove: 51 | del self.assocs[handle] 52 | return len(remove), len(self.assocs) 53 | 54 | 55 | class MemoryStore(object): 56 | """In-process memory store. 57 | 58 | Use for single long-running processes. No persistence supplied. 59 | """ 60 | 61 | def __init__(self): 62 | self.server_assocs = {} 63 | self.nonces = {} 64 | 65 | def _getServerAssocs(self, server_url): 66 | try: 67 | return self.server_assocs[server_url] 68 | except KeyError: 69 | assocs = self.server_assocs[server_url] = ServerAssocs() 70 | return assocs 71 | 72 | def storeAssociation(self, server_url, assoc): 73 | assocs = self._getServerAssocs(server_url) 74 | assocs.set(copy.deepcopy(assoc)) 75 | 76 | def getAssociation(self, server_url, handle=None): 77 | assocs = self._getServerAssocs(server_url) 78 | if handle is None: 79 | return assocs.best() 80 | else: 81 | return assocs.get(handle) 82 | 83 | def removeAssociation(self, server_url, handle): 84 | assocs = self._getServerAssocs(server_url) 85 | return assocs.remove(handle) 86 | 87 | def useNonce(self, server_url, timestamp, salt): 88 | if abs(timestamp - time.time()) > nonce.SKEW: 89 | return False 90 | 91 | anonce = (six.text_type(server_url), int(timestamp), six.text_type(salt)) 92 | if anonce in self.nonces: 93 | return False 94 | else: 95 | self.nonces[anonce] = None 96 | return True 97 | 98 | def cleanupNonces(self): 99 | now = time.time() 100 | expired = [] 101 | for anonce in self.nonces: 102 | if abs(anonce[1] - now) > nonce.SKEW: 103 | # removing items while iterating over the set could be bad. 104 | expired.append(anonce) 105 | 106 | for anonce in expired: 107 | del self.nonces[anonce] 108 | return len(expired) 109 | 110 | def cleanupAssociations(self): 111 | remove_urls = [] 112 | removed_assocs = 0 113 | for server_url, assocs in six.iteritems(self.server_assocs): 114 | removed, remaining = assocs.cleanup() 115 | removed_assocs += removed 116 | if not remaining: 117 | remove_urls.append(server_url) 118 | 119 | # Remove entries from server_assocs that had none remaining. 120 | for server_url in remove_urls: 121 | del self.server_assocs[server_url] 122 | return removed_assocs 123 | 124 | def __eq__(self, other): 125 | return ((self.server_assocs == other.server_assocs) and (self.nonces == other.nonces)) 126 | 127 | def __ne__(self, other): 128 | return not (self == other) 129 | -------------------------------------------------------------------------------- /openid/kvform.py: -------------------------------------------------------------------------------- 1 | """Utilities for key-value format conversions.""" 2 | from __future__ import unicode_literals 3 | 4 | import logging 5 | 6 | import six 7 | 8 | from .oidutil import string_to_text 9 | 10 | __all__ = ['seqToKV', 'kvToSeq', 'dictToKV', 'kvToDict'] 11 | 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | class KVFormError(ValueError): 17 | pass 18 | 19 | 20 | def seqToKV(seq, strict=False): 21 | """Represent a sequence of pairs of strings as newline-terminated 22 | key:value pairs. The pairs are generated in the order given. 23 | 24 | @param seq: The pairs 25 | @type seq: List[Tuple[six.text_type, six.text_type]], binary_type values are deprecated. 26 | 27 | @return: A string representation of the sequence 28 | @rtype: six.text_type 29 | """ 30 | def err(msg): 31 | formatted = 'seqToKV warning: %s: %r' % (msg, seq) 32 | if strict: 33 | raise KVFormError(formatted) 34 | else: 35 | _LOGGER.debug(formatted) 36 | 37 | lines = [] 38 | for k, v in seq: 39 | if not isinstance(k, (six.text_type, six.binary_type)): 40 | err('Converting key to text: %r' % k) 41 | k = six.text_type(k) 42 | if not isinstance(v, (six.text_type, six.binary_type)): 43 | err('Converting value to text: %r' % v) 44 | v = six.text_type(v) 45 | 46 | k = string_to_text(k, "Binary values for keys are deprecated. Use text input instead.") 47 | v = string_to_text(v, "Binary values for values are deprecated. Use text input instead.") 48 | 49 | if '\n' in k: 50 | raise KVFormError( 51 | 'Invalid input for seqToKV: key contains newline: %r' % (k,)) 52 | 53 | if ':' in k: 54 | raise KVFormError( 55 | 'Invalid input for seqToKV: key contains colon: %r' % (k,)) 56 | 57 | if k.strip() != k: 58 | err('Key has whitespace at beginning or end: %r' % (k,)) 59 | 60 | if '\n' in v: 61 | raise KVFormError( 62 | 'Invalid input for seqToKV: value contains newline: %r' % (v,)) 63 | 64 | if v.strip() != v: 65 | err('Value has whitespace at beginning or end: %r' % (v,)) 66 | 67 | lines.append(k + ':' + v + '\n') 68 | 69 | return ''.join(lines) 70 | 71 | 72 | def kvToSeq(data, strict=False): 73 | """ 74 | Parse newline-terminated key:value pair string into a sequence. 75 | 76 | After one parse, seqToKV and kvToSeq are inverses, with no warnings:: 77 | 78 | seq = kvToSeq(s) 79 | seqToKV(kvToSeq(seq)) == seq 80 | 81 | @type data: six.text_type, six.binary_type is deprecated 82 | 83 | @rtype: List[Tuple[six.text_type, six.text_type]] 84 | """ 85 | def err(msg): 86 | formatted = 'kvToSeq warning: %s: %r' % (msg, data) 87 | if strict: 88 | raise KVFormError(formatted) 89 | else: 90 | _LOGGER.debug(formatted) 91 | 92 | data = string_to_text(data, "Binary values for data are deprecated. Use text input instead.") 93 | 94 | lines = data.split('\n') 95 | if lines[-1]: 96 | err('Does not end in a newline') 97 | else: 98 | del lines[-1] 99 | 100 | pairs = [] 101 | line_num = 0 102 | for line in lines: 103 | line_num += 1 104 | 105 | # Ignore blank lines 106 | if not line.strip(): 107 | continue 108 | 109 | pair = line.split(':', 1) 110 | if len(pair) == 2: 111 | k, v = pair 112 | k_s = k.strip() 113 | if k_s != k: 114 | fmt = ('In line %d, ignoring leading or trailing ' 115 | 'whitespace in key %r') 116 | err(fmt % (line_num, k)) 117 | 118 | if not k_s: 119 | err('In line %d, got empty key' % (line_num,)) 120 | 121 | v_s = v.strip() 122 | if v_s != v: 123 | fmt = ('In line %d, ignoring leading or trailing ' 124 | 'whitespace in value %r') 125 | err(fmt % (line_num, v)) 126 | 127 | pairs.append((k_s, v_s)) 128 | else: 129 | err('Line %d does not contain a colon' % line_num) 130 | 131 | return pairs 132 | 133 | 134 | def dictToKV(d): 135 | seq = sorted(d.items()) 136 | return seqToKV(seq) 137 | 138 | 139 | def kvToDict(s): 140 | return dict(kvToSeq(s)) 141 | -------------------------------------------------------------------------------- /openid/test/discoverdata.py: -------------------------------------------------------------------------------- 1 | """Module to make discovery data test cases available""" 2 | from __future__ import unicode_literals 3 | 4 | import os.path 5 | 6 | from six.moves.urllib.parse import urljoin 7 | 8 | from openid.yadis.constants import YADIS_HEADER_NAME 9 | from openid.yadis.discover import DiscoveryFailure, DiscoveryResult 10 | 11 | tests_dir = os.path.dirname(__file__) 12 | data_path = os.path.join(tests_dir, 'data') 13 | 14 | testlist = [ 15 | # success, input_name, id_name, result_name 16 | (True, "equiv", "equiv", "xrds"), 17 | (True, "header", "header", "xrds"), 18 | (True, "lowercase_header", "lowercase_header", "xrds"), 19 | (True, "xrds", "xrds", "xrds"), 20 | (True, "xrds_ctparam", "xrds_ctparam", "xrds_ctparam"), 21 | (True, "xrds_ctcase", "xrds_ctcase", "xrds_ctcase"), 22 | (False, "xrds_html", "xrds_html", "xrds_html"), 23 | (True, "redir_equiv", "equiv", "xrds"), 24 | (True, "redir_header", "header", "xrds"), 25 | (True, "redir_xrds", "xrds", "xrds"), 26 | (False, "redir_xrds_html", "xrds_html", "xrds_html"), 27 | (True, "redir_redir_equiv", "equiv", "xrds"), 28 | (False, "404_server_response", None, None), 29 | (False, "404_with_header", None, None), 30 | (False, "404_with_meta", None, None), 31 | (False, "201_server_response", None, None), 32 | (False, "500_server_response", None, None), 33 | ] 34 | 35 | 36 | def getDataName(*components): 37 | sanitized = [] 38 | for part in components: 39 | if part in ['.', '..']: 40 | raise ValueError 41 | elif part: 42 | sanitized.append(part) 43 | 44 | if not sanitized: 45 | raise ValueError 46 | 47 | return os.path.join(data_path, *sanitized) 48 | 49 | 50 | def getExampleXRDS(): 51 | filename = getDataName('example-xrds.xml') 52 | return open(filename).read() 53 | 54 | 55 | example_xrds = getExampleXRDS() 56 | default_test_file = getDataName('test1-discover.txt') 57 | 58 | discover_tests = {} 59 | 60 | 61 | def readTests(filename): 62 | with open(filename) as data_file: 63 | data = data_file.read() 64 | tests = {} 65 | for case in data.split('\f\n'): 66 | (name, content) = case.split('\n', 1) 67 | tests[name] = content 68 | return tests 69 | 70 | 71 | def getData(filename, name): 72 | global discover_tests 73 | try: 74 | file_tests = discover_tests[filename] 75 | except KeyError: 76 | file_tests = discover_tests[filename] = readTests(filename) 77 | return file_tests[name] 78 | 79 | 80 | def fillTemplate(test_name, template, base_url, example_xrds): 81 | mapping = [ 82 | ('URL_BASE/', base_url), 83 | ('', example_xrds), 84 | ('YADIS_HEADER', YADIS_HEADER_NAME), 85 | ('NAME', test_name), 86 | ] 87 | 88 | for k, v in mapping: 89 | template = template.replace(k, v) 90 | 91 | return template 92 | 93 | 94 | def generateSample(test_name, base_url, 95 | example_xrds=example_xrds, 96 | filename=default_test_file): 97 | try: 98 | template = getData(filename, test_name) 99 | except IOError as why: 100 | import errno 101 | if why[0] == errno.ENOENT: 102 | raise KeyError(filename) 103 | else: 104 | raise 105 | 106 | return fillTemplate(test_name, template, base_url, example_xrds) 107 | 108 | 109 | def generateResult(base_url, input_name, id_name, result_name, success): 110 | input_url = urljoin(base_url, input_name) 111 | 112 | # If the name is None then we expect the protocol to fail, which 113 | # we represent by None 114 | if id_name is None: 115 | assert result_name is None 116 | return input_url, DiscoveryFailure 117 | 118 | result = generateSample(result_name, base_url) 119 | headers, content = result.split('\n\n', 1) 120 | header_lines = headers.split('\n') 121 | for header_line in header_lines: 122 | if header_line.startswith('Content-Type:'): 123 | _, ctype = header_line.split(':', 1) 124 | ctype = ctype.strip() 125 | break 126 | else: 127 | ctype = None 128 | 129 | id_url = urljoin(base_url, id_name) 130 | 131 | result = DiscoveryResult(input_url) 132 | result.normalized_uri = id_url 133 | if success: 134 | result.xrds_uri = urljoin(base_url, result_name) 135 | result.content_type = ctype 136 | result.response_text = content.encode('utf-8') 137 | return input_url, result 138 | -------------------------------------------------------------------------------- /openid/yadis/xri.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: openid.test.test_xri -*- 2 | """Utility functions for handling XRIs. 3 | 4 | @see: XRI Syntax v2.0 at the 5 | U{OASIS XRI Technical Committee} 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | import re 10 | 11 | from six.moves.urllib.parse import quote 12 | 13 | from openid.oidutil import string_to_text 14 | from openid.urinorm import GEN_DELIMS, PERCENT_ENCODING_CHARACTER, SUB_DELIMS 15 | 16 | XRI_AUTHORITIES = ['!', '=', '@', '+', '$', '('] 17 | 18 | 19 | def identifierScheme(identifier): 20 | """Determine if this identifier is an XRI or URI. 21 | 22 | @returns: C{"XRI"} or C{"URI"} 23 | """ 24 | if identifier.startswith('xri://') or (identifier and identifier[0] in XRI_AUTHORITIES): 25 | return "XRI" 26 | else: 27 | return "URI" 28 | 29 | 30 | def toIRINormal(xri): 31 | """Transform an XRI to IRI-normal form.""" 32 | if not xri.startswith('xri://'): 33 | xri = 'xri://' + xri 34 | return escapeForIRI(xri) 35 | 36 | 37 | _xref_re = re.compile(r'\((.*?)\)') 38 | 39 | 40 | def _escape_xref(xref_match): 41 | """Escape things that need to be escaped if they're in a cross-reference. 42 | """ 43 | xref = xref_match.group() 44 | xref = xref.replace('/', '%2F') 45 | xref = xref.replace('?', '%3F') 46 | xref = xref.replace('#', '%23') 47 | return xref 48 | 49 | 50 | def escapeForIRI(xri): 51 | """Escape things that need to be escaped when transforming to an IRI.""" 52 | xri = xri.replace('%', '%25') 53 | xri = _xref_re.sub(_escape_xref, xri) 54 | return xri 55 | 56 | 57 | def toURINormal(xri): 58 | """Transform an XRI to URI normal form.""" 59 | return iriToURI(toIRINormal(xri)) 60 | 61 | 62 | def iriToURI(iri): 63 | """Transform an IRI to a URI by escaping unicode. 64 | 65 | According to RFC 3987, section 3.1, "Mapping of IRIs to URIs" 66 | 67 | @type iri: six.text_type, six.binary_type deprecated. 68 | @rtype: six.text_type 69 | """ 70 | iri = string_to_text(iri, "Binary input for iriToURI is deprecated. Use text input instead.") 71 | 72 | # This is hackish. `quote` requires `str` in both py27 and py3+. 73 | if isinstance(iri, str): 74 | # Python 3 branch 75 | return quote(iri, GEN_DELIMS + SUB_DELIMS + PERCENT_ENCODING_CHARACTER) 76 | else: 77 | # Python 2 branch 78 | return quote(iri.encode('utf-8'), 79 | (GEN_DELIMS + SUB_DELIMS + PERCENT_ENCODING_CHARACTER).encode('utf-8')).decode('utf-8') 80 | 81 | 82 | def providerIsAuthoritative(providerID, canonicalID): 83 | """Is this provider ID authoritative for this XRI? 84 | 85 | @returntype: bool 86 | """ 87 | # XXX: can't use rsplit until we require python >= 2.4. 88 | lastbang = canonicalID.rindex('!') 89 | parent = canonicalID[:lastbang] 90 | return parent == providerID 91 | 92 | 93 | def rootAuthority(xri): 94 | """Return the root authority for an XRI. 95 | 96 | Example:: 97 | 98 | rootAuthority("xri://@example") == "xri://@" 99 | 100 | @type xri: six.text_type 101 | @returntype: six.text_type 102 | """ 103 | if xri.startswith('xri://'): 104 | xri = xri[6:] 105 | authority = xri.split('/', 1)[0] 106 | if authority[0] == '(': 107 | # Cross-reference. 108 | # XXX: This is incorrect if someone nests cross-references so there 109 | # is another close-paren in there. Hopefully nobody does that 110 | # before we have a real xriparse function. Hopefully nobody does 111 | # that *ever*. 112 | root = authority[:authority.index(')') + 1] 113 | elif authority[0] in XRI_AUTHORITIES: 114 | # Other XRI reference. 115 | root = authority[0] 116 | else: 117 | # IRI reference. XXX: Can IRI authorities have segments? 118 | segments = authority.split('!') 119 | segments = [c for s in segments for c in s.split('*')] 120 | root = segments[0] 121 | 122 | return XRI(root) 123 | 124 | 125 | def XRI(xri): 126 | """An XRI object allowing comparison of XRI. 127 | 128 | Ideally, this would do full normalization and provide comparsion 129 | operators as per XRI Syntax. Right now, it just does a bit of 130 | canonicalization by ensuring the xri scheme is present. 131 | 132 | @param xri: an xri string 133 | @type xri: six.text_type 134 | """ 135 | if not xri.startswith('xri://'): 136 | xri = 'xri://' + xri 137 | return xri 138 | -------------------------------------------------------------------------------- /background-associations.txt: -------------------------------------------------------------------------------- 1 | Background association requests 2 | ############################### 3 | 4 | This document describes how to make signing in with OpenID faster for 5 | users of your application by never making the users wait for an 6 | association to be made, but using associations when they're 7 | available. Most OpenID libraries and applications attempt to make 8 | associations during the discovery phase of the OpenID authentication 9 | request. Because association requests may have to do Diffie-Hellman 10 | key exchange, which is time consuming. Even if Diffie-Hellman key 11 | exchange is not used, the user still needs to wait for the association 12 | request. 13 | 14 | Setting up your application to make associations in the background 15 | ================================================================== 16 | 17 | When making associations background, there are two components that 18 | need access to the OpenID association store: the consumer application 19 | and the background association fetcher. The consumer needs to be set 20 | up to record the server URL for any request for which an association 21 | does not exist or is expired instead of making a new association. The 22 | background fetcher looks at the server URL queue and makes 23 | associations for any server URLs that need them. After the 24 | associations are made, the consumer will use them until they expire 25 | again. While associations are expired or missing, the consumer will 26 | use stateless mode to complete authentications with the servers that 27 | need associations. 28 | 29 | The OpenID server endpoint URL queue 30 | ----------------------------------------------------------------- 31 | 32 | You will have to set up a conduit between the consumer and the 33 | background association fetcher so that the background association 34 | fetcher knows what servers need associations. The background 35 | association fetcher will not fetch associations for servers that 36 | already have them, so the queue does not have to be very smart. It 37 | could be as simple as a file to which the server URLs are 38 | appended. Either way, the queue needs to be write-able by the consumer 39 | and readable by the background fetcher. 40 | 41 | Configuring the consumer 42 | ----------------------------------------------------------------- 43 | 44 | Create a subclass of ``GenericConsumer`` that overrides 45 | ``_negotiateAssociation`` so that it just records the server URL that 46 | needs an association:: 47 | 48 | from openid.consumer.consumer import GenericConsumer, Consumer 49 | 50 | class LazyAssociationConsumer(GenericConsumer): 51 | needs_assoc_file = None 52 | 53 | def _negotiateAssociation(self, endpoint): 54 | # Do whatever you need to do here to send the server_url to 55 | # the queue. This example just appends it to a file. 56 | self.needs_assoc_file.write(endpoint.server_url + '\n') 57 | self.needs_assoc_file.flush() 58 | 59 | You could also store the whole endpoint object. When you instantiate 60 | the consumer, pass this generic consumer class to the controlling 61 | consumer:: 62 | 63 | return Consumer(session, store, consumer_class=LazyAssociationConsumer) 64 | 65 | The background association fetcher 66 | ----------------------------------------------------------------- 67 | 68 | The background association fetcher is just a script that should be 69 | added to ``cron`` or triggered periodically. If you are ambitious, you 70 | could make the background fetcher listen for inserts into the queue. 71 | 72 | The background fetcher needs to do something similar to the following:: 73 | 74 | def refresh(consumer, endpoint): 75 | if consumer.store.getAssociation(endpoint.server_url): 76 | logging.info("We don't need to associate with %r", endpoint.server_url) 77 | return 78 | 79 | logging.info("Associating with %r", endpoint.server_url) 80 | now = time.time() 81 | assoc = consumer._negotiateAssociation(endpoint) 82 | if assoc: 83 | elapsed = time.time() - now 84 | logging.info('(%0.2f seconds) Associated with %r', elapsed, 85 | endpoint.server_url) 86 | consumer.store.storeAssociation(endpoint.server_url, assoc) 87 | else: 88 | logging.error('Failed to make an association with %r', 89 | endpoint.server_url) 90 | 91 | The code in this example logs the amount of time that the association 92 | request took. This is time that the user would have been waiting. The 93 | ``consumer`` in this example is a standard consumer, not the 94 | ``LazyAssociationConsumer`` that was defined in the section 95 | above. This is important, because the lazy consumer above will not 96 | actually make any associations. 97 | -------------------------------------------------------------------------------- /openid/test/data/test_etxrd/ref.xrds: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | *ootao 5 | 6 | 2006-08-15T18:56:09.000Z 7 | xri://@ 8 | !5BAD.2AA.3C72.AF46 9 | @!5BAD.2AA.3C72.AF46 10 | 11 | xri://$res*auth*($v*2.0) 12 | 13 | application/xrds+xml;trust=none 14 | http://resolve.ezibroker.net/resolve/@ootao/ 15 | 16 | 17 | http://openid.net/signon/1.0 18 | 19 | https://linksafe.ezibroker.net/server/ 20 | 21 | 22 | 23 | *test.ref 24 | SUCCESS 25 | xri://!!1003 26 | !0000.0000.3B9A.CA03 27 | @!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA03 28 | @!BAE.A650.823B.2475 29 | 30 | http://openid.net/signon/1.0 31 | 32 | https://linksafe.ezibroker.net/server/ 33 | 34 | 35 | 36 | 37 | !BAE.A650.823B.2475 38 | 39 | 2006-08-15T18:56:10.000Z 40 | xri://@ 41 | !BAE.A650.823B.2475 42 | @!BAE.A650.823B.2475 43 | 44 | (+wdnc) 45 | 46 | (+wdnc) 47 | http://www.tcpacompliance.us 48 | 49 | 50 | xri://$res*auth*($v*2.0) 51 | 52 | application/xrds+xml;trust=none 53 | http://dev.dready.org/cgi-bin/xri 54 | 55 | 56 | (+i-name) 57 | 58 | (+i-name) 59 | http://www.inames.net 60 | 61 | 62 | xri://+i-service*(+contact)*($v*1.0) 63 | 64 | xri://!!1001 65 | (+contact) 66 | 67 | text/html 68 | 69 | http://www.neustar.biz 70 | 71 | 72 | 73 | 74 | !BAE.A650.823B.2475 75 | 76 | 2006-08-15T18:56:10.000Z 77 | xri://@ 78 | !BAE.A650.823B.2475 79 | @!BAE.A650.823B.2475 80 | 81 | (+wdnc) 82 | 83 | (+wdnc) 84 | http://www.tcpacompliance.us 85 | 86 | 87 | xri://$res*auth*($v*2.0) 88 | 89 | application/xrds+xml;trust=none 90 | http://dev.dready.org/cgi-bin/xri 91 | 92 | 93 | (+i-name) 94 | 95 | (+i-name) 96 | http://www.inames.net 97 | 98 | 99 | xri://+i-service*(+contact)*($v*1.0) 100 | 101 | xri://!!1001 102 | (+contact) 103 | 104 | text/html 105 | 106 | http://www.neustar.biz 107 | 108 | 109 | -------------------------------------------------------------------------------- /openid/test/test_urinorm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for `openid.urinorm` module.""" 3 | from __future__ import unicode_literals 4 | 5 | import unittest 6 | import warnings 7 | 8 | import six 9 | from testfixtures import ShouldWarn 10 | 11 | from openid.urinorm import urinorm 12 | 13 | 14 | class UrinormTest(unittest.TestCase): 15 | """Test `urinorm` function.""" 16 | 17 | def test_normalized(self): 18 | self.assertEqual(urinorm('http://example.com/'), 'http://example.com/') 19 | warning_msg = "Binary input for urinorm is deprecated. Use text input instead." 20 | with ShouldWarn(DeprecationWarning(warning_msg)): 21 | warnings.simplefilter('always') 22 | self.assertEqual(urinorm(b'http://example.com/'), 'http://example.com/') 23 | 24 | def test_lowercase_scheme(self): 25 | self.assertEqual(urinorm('htTP://example.com/'), 'http://example.com/') 26 | 27 | def test_unsupported_scheme(self): 28 | six.assertRaisesRegex(self, ValueError, 'Not an absolute HTTP or HTTPS URI', urinorm, 'ftp://example.com/') 29 | 30 | def test_lowercase_hostname(self): 31 | self.assertEqual(urinorm('http://exaMPLE.COm/'), 'http://example.com/') 32 | 33 | def test_idn_hostname(self): 34 | self.assertEqual(urinorm('http://π.example.com/'), 'http://xn--1xa.example.com/') 35 | 36 | def test_empty_hostname(self): 37 | self.assertEqual(urinorm('http://username@/'), 'http://username@/') 38 | 39 | def test_invalid_hostname(self): 40 | six.assertRaisesRegex(self, ValueError, 'Invalid hostname', urinorm, 'http://.it/') 41 | six.assertRaisesRegex(self, ValueError, 'Invalid hostname', urinorm, 'http://..it/') 42 | six.assertRaisesRegex(self, ValueError, 'Not an absolute URI', urinorm, 'http:///path/') 43 | 44 | def test_empty_port_section(self): 45 | self.assertEqual(urinorm('http://example.com:/'), 'http://example.com/') 46 | 47 | def test_default_ports(self): 48 | self.assertEqual(urinorm('http://example.com:80/'), 'http://example.com/') 49 | self.assertEqual(urinorm('https://example.com:443/'), 'https://example.com/') 50 | 51 | def test_empty_path(self): 52 | self.assertEqual(urinorm('http://example.com'), 'http://example.com/') 53 | 54 | def test_path_dots(self): 55 | self.assertEqual(urinorm('http://example.com/./a'), 'http://example.com/a') 56 | self.assertEqual(urinorm('http://example.com/../a'), 'http://example.com/a') 57 | 58 | self.assertEqual(urinorm('http://example.com/a/.'), 'http://example.com/a/') 59 | self.assertEqual(urinorm('http://example.com/a/..'), 'http://example.com/') 60 | self.assertEqual(urinorm('http://example.com/a/./'), 'http://example.com/a/') 61 | self.assertEqual(urinorm('http://example.com/a/../'), 'http://example.com/') 62 | 63 | self.assertEqual(urinorm('http://example.com/a/./b'), 'http://example.com/a/b') 64 | self.assertEqual(urinorm('http://example.com/a/../b'), 'http://example.com/b') 65 | 66 | self.assertEqual(urinorm('http://example.com/a/b/c/./../../g'), 'http://example.com/a/g') 67 | self.assertEqual(urinorm('http://example.com/mid/content=5/../6'), 'http://example.com/mid/6') 68 | 69 | def test_path_percent_encoding(self): 70 | self.assertEqual(urinorm('http://example.com/'), 'http://example.com/%08') 71 | self.assertEqual(urinorm('http://example.com/Λ'), 'http://example.com/%CE%9B') 72 | 73 | def test_path_capitalize_percent_encoding(self): 74 | self.assertEqual(urinorm('http://example.com/foo%3abar'), 'http://example.com/foo%3Abar') 75 | 76 | def test_path_percent_decode_unreserved(self): 77 | self.assertEqual(urinorm('http://example.com/foo%2Dbar%2dbaz'), 'http://example.com/foo-bar-baz') 78 | 79 | def test_path_keep_sub_delims(self): 80 | self.assertEqual(urinorm('http://example.com/foo+!bar'), 'http://example.com/foo+!bar') 81 | 82 | def test_path_percent_decode_sub_delims(self): 83 | self.assertEqual(urinorm('http://example.com/foo%2B%21bar'), 'http://example.com/foo+!bar') 84 | 85 | def test_query_encoding(self): 86 | self.assertEqual( 87 | urinorm('http://example.com/?openid.sreg.fullname=Unícöde+Person'), 88 | 'http://example.com/?openid.sreg.fullname=Un%C3%ADc%C3%B6de+Person') 89 | self.assertEqual( 90 | urinorm('http://example.com/?openid.sreg.fullname=Un%C3%ADc%C3%B6de+Person'), 91 | 'http://example.com/?openid.sreg.fullname=Un%C3%ADc%C3%B6de+Person') 92 | 93 | def test_illegal_characters(self): 94 | six.assertRaisesRegex(self, ValueError, 'Illegal characters in URI', urinorm, 'http://.com/') 95 | 96 | def test_realms(self): 97 | # Urinorm supports OpenID realms with * in them 98 | self.assertEqual(urinorm('http://*.example.com/'), 'http://*.example.com/') 99 | -------------------------------------------------------------------------------- /contrib/openid-parse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Grab URLs from the clipboard, interpret the queries as OpenID, and print. 3 | 4 | In addition to URLs, I also scan for queries as they appear in httpd log files, 5 | with a pattern like 'GET /foo?bar=baz HTTP'. 6 | 7 | Requires the 'xsel' program to get the contents of the clipboard. 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | import re 12 | import subprocess 13 | import sys 14 | from pprint import pformat 15 | 16 | import six 17 | from six.moves.urllib.parse import parse_qs, urlsplit, urlunsplit 18 | 19 | from openid import message 20 | 21 | OPENID_SORT_ORDER = ['mode', 'identity', 'claimed_id'] 22 | 23 | 24 | class NoQuery(Exception): 25 | def __init__(self, url): 26 | self.url = url 27 | 28 | def __str__(self): 29 | return "No query in url %s" % (self.url,) 30 | 31 | 32 | def getClipboard(): 33 | xsel = subprocess.Popen(["xsel", "-o", "-b"], stdout=subprocess.PIPE) 34 | output = xsel.communicate()[0] 35 | return output 36 | 37 | 38 | def main(): 39 | source = getClipboard() 40 | urls = find_urls(source) 41 | 42 | errors = [] 43 | output = [] 44 | queries = [] 45 | 46 | queries.extend(queriesFromPostdata(source)) 47 | 48 | for url in urls: 49 | try: 50 | queries.append(queryFromURL(url)) 51 | except NoQuery as err: 52 | errors.append(err) 53 | 54 | queries.extend(queriesFromLogs(source)) 55 | 56 | for where, query in queries: 57 | output.append('at %s:\n%s' % (where, openidFromQuery(query))) 58 | 59 | if output: 60 | print('\n\n'.join(output)) 61 | elif errors: 62 | for err in errors: 63 | print(err) 64 | 65 | 66 | def queryFromURL(url): 67 | split_url = urlsplit(url) 68 | query = parse_qs(split_url[3]) 69 | 70 | if not query: 71 | raise NoQuery(url) 72 | 73 | url_without_query = urlunsplit(split_url[:3] + (None, None)) 74 | 75 | return (url_without_query, query) 76 | 77 | 78 | def openidFromQuery(query): 79 | try: 80 | msg = message.Message.fromPostArgs(unlistify(query)) 81 | s = formatOpenIDMessage(msg) 82 | except Exception as err: 83 | # XXX - side effect. 84 | sys.stderr.write(six.text_type(err)) 85 | s = pformat(query) 86 | 87 | return s 88 | 89 | 90 | def formatOpenIDMessage(msg): 91 | value_lists = {} 92 | for (ns_uri, ns_key), value in msg.args.items(): 93 | l = value_lists.setdefault(ns_uri, {}) 94 | l[ns_key] = value 95 | 96 | output = [] 97 | 98 | for ns_uri, values in value_lists.items(): 99 | ns_output = [] 100 | 101 | alias = msg.namespaces.getAlias(ns_uri) 102 | if alias is message.NULL_NAMESPACE: 103 | alias = 'openid' 104 | ns_output.append(" %s <%s>" % (alias, ns_uri)) 105 | 106 | for key in OPENID_SORT_ORDER: 107 | try: 108 | ns_output.append(" %s = %s" % (key, values.pop(key))) 109 | except KeyError: 110 | pass 111 | 112 | values = sorted(values.items()) 113 | 114 | for k, v in values: 115 | ns_output.append(" %s = %s" % (k, v)) 116 | 117 | output.append('\n'.join(ns_output)) 118 | 119 | return '\n\n'.join(output) 120 | 121 | 122 | def unlistify(d): 123 | return dict((i[0], i[1][0]) for i in d.items()) 124 | 125 | 126 | def queriesFromLogs(s): 127 | qre = re.compile(r'GET (/.*)?\?(.+) HTTP') 128 | 129 | return [(match.group(1), parse_qs(match.group(2))) 130 | for match in qre.finditer(s)] 131 | 132 | 133 | def queriesFromPostdata(s): 134 | # This looks for query data in a line that starts POSTDATA=. 135 | # Tamperdata outputs such lines. If there's a 'Host=' in that block, 136 | # use that too, but don't require it. 137 | qre = re.compile(r'(?:^Host=(?P.+?)$.*?)?^POSTDATA=(?P.*)$', 138 | re.DOTALL | re.MULTILINE) 139 | return [(match.group('host') or 'POSTDATA', 140 | parse_qs(match.group('query'))) for match in qre.finditer(s)] 141 | 142 | 143 | def find_urls(s): 144 | # Regular expression borrowed from urlscan 145 | # by Daniel Burrows , GPL. 146 | urlinternalpattern = r'[{}a-zA-Z/\-_0-9%?&.=:;+,#~]' 147 | urltrailingpattern = r'[{}a-zA-Z/\-_0-9%&=+#]' 148 | httpurlpattern = r'(?:https?://' + urlinternalpattern + r'*' + urltrailingpattern + r')' 149 | # Used to guess that blah.blah.blah.TLD is a URL. 150 | tlds = ['biz', 'com', 'edu', 'info', 'org'] 151 | guessedurlpattern = r'(?:[a-zA-Z0-9_\-%]+(?:\.[a-zA-Z0-9_\-%]+)*\.(?:' + '|'.join(tlds) + '))' 152 | urlre = re.compile(r'(?:<(?:URL:)?)?(' + httpurlpattern + '|' + guessedurlpattern + 153 | '|(?:mailto:[a-zA-Z0-9\-_]*@[0-9a-zA-Z_\-.]*[0-9a-zA-Z_\-]))>?') 154 | 155 | return [match.group(1) for match in urlre.finditer(s)] 156 | 157 | 158 | if __name__ == '__main__': 159 | main() 160 | -------------------------------------------------------------------------------- /openid/yadis/xrires.py: -------------------------------------------------------------------------------- 1 | """XRI resolution.""" 2 | from __future__ import unicode_literals 3 | 4 | from six.moves.urllib.parse import urlencode 5 | 6 | from openid import fetchers 7 | from openid.oidutil import string_to_text 8 | from openid.yadis import etxrd 9 | from openid.yadis.services import iterServices 10 | from openid.yadis.xri import toURINormal 11 | 12 | DEFAULT_PROXY = 'http://proxy.xri.net/' 13 | 14 | 15 | class ProxyResolver(object): 16 | """Python interface to a remote XRI proxy resolver. 17 | """ 18 | 19 | def __init__(self, proxy_url=DEFAULT_PROXY): 20 | self.proxy_url = proxy_url 21 | 22 | def queryURL(self, xri, service_type=None): 23 | """Build a URL to query the proxy resolver. 24 | 25 | @param xri: An XRI to resolve. 26 | @type xri: six.text_type 27 | 28 | @param service_type: The service type to resolve, if you desire 29 | service endpoint selection. A service type is a URI. 30 | @type service_type: Optional[six.text_type], six.binary_type is deprecated 31 | 32 | @returns: a URL 33 | @returntype: six.text_type 34 | """ 35 | # Trim off the xri:// prefix. The proxy resolver didn't accept it 36 | # when this code was written, but that may (or may not) change for 37 | # XRI Resolution 2.0 Working Draft 11. 38 | qxri = toURINormal(xri)[6:] 39 | hxri = self.proxy_url + qxri 40 | args = { 41 | # XXX: If the proxy resolver will ensure that it doesn't return 42 | # bogus CanonicalIDs (as per Steve's message of 15 Aug 2006 43 | # 11:13:42), then we could ask for application/xrd+xml instead, 44 | # which would give us a bit less to process. 45 | '_xrd_r': 'application/xrds+xml', 46 | } 47 | if service_type: 48 | service_type = string_to_text(service_type, 49 | "Binary values for service_type are deprecated. Use text input instead.") 50 | args['_xrd_t'] = service_type 51 | else: 52 | # Don't perform service endpoint selection. 53 | args['_xrd_r'] += ';sep=false' 54 | query = _appendArgs(hxri, args) 55 | return query 56 | 57 | def query(self, xri, service_types): 58 | """Resolve some services for an XRI. 59 | 60 | Note: I don't implement any service endpoint selection beyond what 61 | the resolver I'm querying does, so the Services I return may well 62 | include Services that were not of the types you asked for. 63 | 64 | May raise fetchers.HTTPFetchingError or L{etxrd.XRDSError} if 65 | the fetching or parsing don't go so well. 66 | 67 | @param xri: An XRI to resolve. 68 | @type xri: six.text_type 69 | 70 | @param service_types: A list of services types to query for. Service 71 | types are URIs. 72 | @type service_types: List[six.text_type], six.binary_type is deprecated 73 | 74 | @returns: tuple of (CanonicalID, Service elements) 75 | @returntype: (six.text_type, list of C{ElementTree.Element}s) 76 | """ 77 | # FIXME: No test coverage! 78 | services = [] 79 | # Make a seperate request to the proxy resolver for each service 80 | # type, as, if it is following Refs, it could return a different 81 | # XRDS for each. 82 | 83 | canonicalID = None 84 | 85 | for service_type in service_types: 86 | url = self.queryURL(xri, service_type) 87 | response = fetchers.fetch(url) 88 | if response.status not in (200, 206): 89 | # XXX: sucks to fail silently. 90 | # print("response not OK:", response) 91 | continue 92 | et = etxrd.parseXRDS(response.body) 93 | canonicalID = etxrd.getCanonicalID(xri, et) 94 | some_services = list(iterServices(et)) 95 | services.extend(some_services) 96 | # TODO: 97 | # * If we do get hits for multiple service_types, we're almost 98 | # certainly going to have duplicated service entries and 99 | # broken priority ordering. 100 | return canonicalID, services 101 | 102 | 103 | def _appendArgs(url, args): 104 | """Append some arguments to an HTTP query. 105 | """ 106 | # to be merged with oidutil.appendArgs when we combine the projects. 107 | if hasattr(args, 'items'): 108 | args = sorted(args.items()) 109 | 110 | if len(args) == 0: 111 | return url 112 | 113 | # According to XRI Resolution section "QXRI query parameters": 114 | # 115 | # """If the original QXRI had a null query component (only a leading 116 | # question mark), or a query component consisting of only question 117 | # marks, one additional leading question mark MUST be added when 118 | # adding any XRI resolution parameters.""" 119 | 120 | if '?' in url.rstrip('?'): 121 | sep = '&' 122 | else: 123 | sep = '?' 124 | 125 | return '%s%s%s' % (url, sep, urlencode(args)) 126 | -------------------------------------------------------------------------------- /openid/test/test_kvform.py: -------------------------------------------------------------------------------- 1 | """Tests for `openid.kvform` module.""" 2 | from __future__ import unicode_literals 3 | 4 | import unittest 5 | 6 | import six 7 | from testfixtures import LogCapture 8 | 9 | from openid import kvform 10 | 11 | 12 | class KVDictTest(unittest.TestCase): 13 | 14 | def runTest(self): 15 | for kv_data, result, expected_warnings in kvdict_cases: 16 | # Convert KVForm to dict 17 | with LogCapture() as logbook: 18 | d = kvform.kvToDict(kv_data) 19 | 20 | # make sure it parses to expected dict 21 | self.assertEqual(d, result) 22 | 23 | # Check to make sure we got the expected number of warnings 24 | self.assertEqual(len(logbook.records), expected_warnings) 25 | 26 | # Convert back to KVForm and round-trip back to dict to make 27 | # sure that *** dict -> kv -> dict is identity. *** 28 | kv = kvform.dictToKV(d) 29 | d2 = kvform.kvToDict(kv) 30 | self.assertEqual(d, d2) 31 | 32 | 33 | class KVSeqTest(unittest.TestCase): 34 | 35 | def cleanSeq(self, seq): 36 | """Create a new sequence by stripping whitespace from start 37 | and end of each value of each pair""" 38 | clean = [] 39 | for k, v in seq: 40 | clean.append((k.strip(), v.strip())) 41 | return clean 42 | 43 | def runTest(self): 44 | for kv_data, result, expected_warnings in kvseq_cases: 45 | # seq serializes to expected kvform 46 | with LogCapture() as logbook: 47 | actual = kvform.seqToKV(kv_data) 48 | self.assertEqual(actual, result) 49 | self.assertIsInstance(actual, six.text_type) 50 | 51 | # Parse back to sequence. Expected to be unchanged, except 52 | # stripping whitespace from start and end of values 53 | # (i. e. ordering, case, and internal whitespace is preserved) 54 | seq = kvform.kvToSeq(actual) 55 | clean_seq = self.cleanSeq(seq) 56 | 57 | self.assertEqual(seq, clean_seq) 58 | self.assertEqual(len(logbook.records), expected_warnings, 59 | "Invalid warnings for {}: {}".format(kv_data, [r.getMessage() for r in logbook.records])) 60 | 61 | 62 | kvdict_cases = [ 63 | # (kvform, parsed dictionary, expected warnings) 64 | ('', {}, 0), 65 | ('college:harvey mudd\n', {'college': 'harvey mudd'}, 0), 66 | ('city:claremont\nstate:CA\n', {'city': 'claremont', 'state': 'CA'}, 0), 67 | ('is_valid:true\ninvalidate_handle:{HMAC-SHA1:2398410938412093}\n', 68 | {'is_valid': 'true', 'invalidate_handle': '{HMAC-SHA1:2398410938412093}'}, 0), 69 | 70 | # Warnings from lines with no colon: 71 | ('x\n', {}, 1), 72 | ('x\nx\n', {}, 2), 73 | ('East is least\n', {}, 1), 74 | 75 | # But not from blank lines (because LJ generates them) 76 | ('x\n\n', {}, 1), 77 | 78 | # Warning from empty key 79 | (':\n', {'': ''}, 1), 80 | (':missing key\n', {'': 'missing key'}, 1), 81 | 82 | # Warnings from leading or trailing whitespace in key or value 83 | (' street:foothill blvd\n', {'street': 'foothill blvd'}, 1), 84 | ('major: computer science\n', {'major': 'computer science'}, 1), 85 | (' dorm : east \n', {'dorm': 'east'}, 2), 86 | 87 | # Warnings from missing trailing newline 88 | ('e^(i*pi)+1:0', {'e^(i*pi)+1': '0'}, 1), 89 | ('east:west\nnorth:south', {'east': 'west', 'north': 'south'}, 1), 90 | ] 91 | 92 | kvseq_cases = [ 93 | ([], '', 0), 94 | 95 | # Make sure that we handle unicode characters 96 | ([('\u03bbx', 'x')], '\u03bbx:x\n', 0), 97 | 98 | # If it's a UTF-8 str, make sure that it's equivalent to the same 99 | # string, decoded. 100 | ([(b'\xce\xbbx', b'x')], '\u03bbx:x\n', 0), 101 | 102 | ([('openid', 'useful'), ('a', 'b')], 'openid:useful\na:b\n', 0), 103 | 104 | # Warnings about leading whitespace 105 | ([(' openid', 'useful'), ('a', 'b')], ' openid:useful\na:b\n', 1), 106 | 107 | # Warnings about leading and trailing whitespace 108 | ([(' openid ', ' useful '), 109 | (' a ', ' b ')], ' openid : useful \n a : b \n', 4), 110 | 111 | # warnings about leading and trailing whitespace, but not about 112 | # internal whitespace. 113 | ([(' open id ', ' use ful '), 114 | (' a ', ' b ')], ' open id : use ful \n a : b \n', 4), 115 | 116 | ([('foo', 'bar')], 'foo:bar\n', 0), 117 | ] 118 | 119 | kvexc_cases = [ 120 | [('openid', 'use\nful')], 121 | [('open\nid', 'useful')], 122 | [('open\nid', 'use\nful')], 123 | [('open:id', 'useful')], 124 | [('foo', 'bar'), ('ba\n d', 'seed')], 125 | [('foo', 'bar'), ('bad:', 'seed')], 126 | ] 127 | 128 | 129 | class KVExcTest(unittest.TestCase): 130 | 131 | def runTest(self): 132 | for kv_data in kvexc_cases: 133 | self.assertRaises(ValueError, kvform.seqToKV, kv_data) 134 | 135 | 136 | class GeneralTest(unittest.TestCase): 137 | kvform = '' 138 | 139 | def test_convert(self): 140 | with LogCapture() as logbook: 141 | result = kvform.seqToKV([(1, 1)]) 142 | self.assertEqual(result, '1:1\n') 143 | self.assertEqual(len(logbook.records), 2) 144 | -------------------------------------------------------------------------------- /openid/test/test_yadis_discover.py: -------------------------------------------------------------------------------- 1 | """Tests for yadis.discover. 2 | 3 | @todo: Now that yadis.discover uses urljr.fetchers, we should be able to do 4 | tests with a mock fetcher instead of spawning threads with BaseHTTPServer. 5 | """ 6 | from __future__ import unicode_literals 7 | 8 | import re 9 | import types 10 | import unittest 11 | 12 | from six.moves.urllib.parse import urlparse 13 | 14 | from openid import fetchers 15 | from openid.yadis.discover import DiscoveryFailure, discover 16 | 17 | from . import discoverdata 18 | 19 | status_header_re = re.compile(r'Status: (\d+) .*?$', re.MULTILINE) 20 | 21 | four04_pat = """\ 22 | Content-Type: text/plain 23 | 24 | No such file %s 25 | """ 26 | 27 | 28 | class QuitServer(Exception): 29 | pass 30 | 31 | 32 | def mkResponse(data): 33 | status_mo = status_header_re.match(data) 34 | headers_str, body = data.split('\n\n', 1) 35 | headers = {} 36 | for line in headers_str.split('\n'): 37 | k, v = line.split(':', 1) 38 | k = k.strip().lower() 39 | v = v.strip() 40 | headers[k] = v 41 | status = int(status_mo.group(1)) 42 | return fetchers.HTTPResponse(status=status, 43 | headers=headers, 44 | body=body.encode('utf-8')) 45 | 46 | 47 | class TestFetcher(object): 48 | def __init__(self, base_url): 49 | self.base_url = base_url 50 | 51 | def fetch(self, url, headers, body): 52 | current_url = url 53 | while True: 54 | parsed = urlparse(current_url) 55 | path = parsed[2][1:] 56 | try: 57 | data = discoverdata.generateSample(path, self.base_url) 58 | except KeyError: 59 | return fetchers.HTTPResponse(status=404, 60 | final_url=current_url, 61 | headers={}, 62 | body='') 63 | 64 | response = mkResponse(data) 65 | if response.status in [301, 302, 303, 307]: 66 | current_url = response.headers['location'] 67 | else: 68 | response.final_url = current_url 69 | return response 70 | 71 | 72 | class TestSecondGet(unittest.TestCase): 73 | class MockFetcher(object): 74 | def __init__(self): 75 | self.count = 0 76 | 77 | def fetch(self, uri, headers=None, body=None): 78 | self.count += 1 79 | if self.count == 1: 80 | headers = { 81 | 'X-XRDS-Location'.lower(): 'http://unittest/404', 82 | } 83 | return fetchers.HTTPResponse(uri, 200, headers, '') 84 | else: 85 | return fetchers.HTTPResponse(uri, 404) 86 | 87 | def setUp(self): 88 | self.oldfetcher = fetchers.getDefaultFetcher() 89 | fetchers.setDefaultFetcher(self.MockFetcher()) 90 | 91 | def tearDown(self): 92 | fetchers.setDefaultFetcher(self.oldfetcher) 93 | 94 | def test_404(self): 95 | uri = "http://something.unittest/" 96 | self.assertRaises(DiscoveryFailure, discover, uri) 97 | 98 | 99 | class TestDiscover(unittest.TestCase): 100 | base_url = 'http://invalid.unittest/' 101 | 102 | def setUp(self): 103 | fetchers.setDefaultFetcher(TestFetcher(self.base_url), 104 | wrap_exceptions=False) 105 | 106 | def tearDown(self): 107 | fetchers.setDefaultFetcher(None) 108 | 109 | def test(self): 110 | for success, input_name, id_name, result_name in discoverdata.testlist: 111 | input_url, expected = discoverdata.generateResult( 112 | self.base_url, 113 | input_name, 114 | id_name, 115 | result_name, 116 | success) 117 | 118 | if expected is DiscoveryFailure: 119 | self.assertRaises(DiscoveryFailure, discover, input_url) 120 | else: 121 | result = discover(input_url) 122 | self.assertEqual(result.request_uri, input_url) 123 | 124 | msg = 'Identity URL mismatch: actual = %r, expected = %r' % ( 125 | result.normalized_uri, expected.normalized_uri) 126 | self.assertEqual(result.normalized_uri, expected.normalized_uri, msg) 127 | 128 | msg = 'Content mismatch: actual = %r, expected = %r' % ( 129 | result.response_text, expected.response_text) 130 | self.assertEqual(result.response_text, expected.response_text, msg) 131 | 132 | expected_keys = sorted(dir(expected)) 133 | actual_keys = sorted(dir(result)) 134 | self.assertEqual(actual_keys, expected_keys) 135 | 136 | for k in dir(expected): 137 | if k.startswith('__') and k.endswith('__'): 138 | continue 139 | exp_v = getattr(expected, k) 140 | if isinstance(exp_v, types.MethodType): 141 | continue 142 | act_v = getattr(result, k) 143 | assert act_v == exp_v, (k, exp_v, act_v) 144 | -------------------------------------------------------------------------------- /openid/test/test_openidyadis.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import unittest 4 | 5 | import six 6 | 7 | from openid.consumer.discover import OPENID_1_0_TYPE, OPENID_1_1_TYPE, OpenIDServiceEndpoint 8 | from openid.yadis.services import applyFilter 9 | 10 | XRDS_BOILERPLATE = '''\ 11 | 12 | 15 | 16 | %s\ 17 | 18 | 19 | ''' 20 | 21 | 22 | def mkXRDS(services): 23 | xrds = XRDS_BOILERPLATE % services 24 | return xrds.encode('utf-8') 25 | 26 | 27 | def mkService(uris=None, type_uris=None, local_id=None, dent=' '): 28 | chunks = [dent, '\n'] 29 | dent2 = dent + ' ' 30 | if type_uris: 31 | for type_uri in type_uris: 32 | chunks.extend([dent2 + '', type_uri, '\n']) 33 | 34 | if uris: 35 | for uri in uris: 36 | if isinstance(uri, tuple): 37 | uri, prio = uri 38 | else: 39 | prio = None 40 | 41 | chunks.extend([dent2, '', uri, '\n']) 45 | 46 | if local_id: 47 | chunks.extend( 48 | [dent2, '', local_id, '\n']) 49 | 50 | chunks.extend([dent, '\n']) 51 | 52 | return ''.join(chunks) 53 | 54 | 55 | # Different sets of server URLs for use in the URI tag 56 | server_url_options = [ 57 | [], # This case should not generate an endpoint object 58 | ['http://server.url/'], 59 | ['https://server.url/'], 60 | ['https://server.url/', 'http://server.url/'], 61 | ['https://server.url/', 62 | 'http://server.url/', 63 | 'http://example.server.url/'], 64 | ] 65 | 66 | # Used for generating test data 67 | 68 | 69 | def subsets(lst): 70 | """Generate all non-empty sublists of a list""" 71 | subsets_list = [[]] 72 | for x in lst: 73 | subsets_list += [[x] + t for t in subsets_list] 74 | return subsets_list 75 | 76 | 77 | # A couple of example extension type URIs. These are not at all 78 | # official, but are just here for testing. 79 | ext_types = [ 80 | 'http://janrain.com/extension/blah', 81 | 'http://openid.net/sreg/1.0', 82 | ] 83 | 84 | # All valid combinations of Type tags that should produce an OpenID endpoint 85 | type_uri_options = [ 86 | exts + ts 87 | 88 | # All non-empty sublists of the valid OpenID type URIs 89 | for ts in subsets([OPENID_1_0_TYPE, OPENID_1_1_TYPE]) 90 | if ts 91 | 92 | # All combinations of extension types (including empty extenstion list) 93 | for exts in subsets(ext_types) 94 | ] 95 | 96 | # Range of valid Delegate tag values for generating test data 97 | local_id_options = [ 98 | None, 99 | 'http://vanity.domain/', 100 | 'https://somewhere/yadis/', 101 | ] 102 | 103 | # All combinations of valid URIs, Type URIs and Delegate tags 104 | data = [ 105 | (uris, type_uris, local_id) 106 | for uris in server_url_options 107 | for type_uris in type_uri_options 108 | for local_id in local_id_options 109 | ] 110 | 111 | 112 | class OpenIDYadisTest(unittest.TestCase): 113 | 114 | yadis_url = 'http://unit.test/' 115 | 116 | def shortDescription(self): 117 | # XXX: 118 | return 'Successful OpenID Yadis parsing case' 119 | 120 | def make_xrds(self, uris, type_uris, local_id): 121 | # Create an XRDS document to parse 122 | services = mkService(uris=uris, 123 | type_uris=type_uris, 124 | local_id=local_id) 125 | return mkXRDS(services) 126 | 127 | def runTest(self): 128 | for uris, type_uris, local_id in data: 129 | # Parse into endpoint objects that we will check 130 | endpoints = applyFilter(self.yadis_url, self.make_xrds(uris, type_uris, local_id), OpenIDServiceEndpoint) 131 | 132 | # make sure there are the same number of endpoints as 133 | # URIs. This assumes that the type_uris contains at least one 134 | # OpenID type. 135 | self.assertEqual(len(endpoints), len(uris)) 136 | 137 | # So that we can check equality on the endpoint types 138 | type_uris = sorted(type_uris) 139 | 140 | seen_uris = [] 141 | for endpoint in endpoints: 142 | seen_uris.append(endpoint.server_url) 143 | 144 | # All endpoints will have same yadis_url 145 | self.assertEqual(endpoint.claimed_id, self.yadis_url) 146 | 147 | # and local_id 148 | self.assertEqual(endpoint.local_id, local_id) 149 | 150 | # and types 151 | actual_types = sorted(endpoint.type_uris) 152 | self.assertEqual(type_uris, actual_types) 153 | 154 | # So that they will compare equal, because we don't care what 155 | # order they are in 156 | seen_uris.sort() 157 | uris = sorted(uris) 158 | 159 | # Make sure we saw all URIs, and saw each one once 160 | self.assertEqual(seen_uris, uris) 161 | -------------------------------------------------------------------------------- /openid/urinorm.py: -------------------------------------------------------------------------------- 1 | """URI normalization utilities.""" 2 | from __future__ import unicode_literals 3 | 4 | import string 5 | 6 | import six 7 | from six.moves.urllib.parse import parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit 8 | 9 | from .oidutil import string_to_text 10 | 11 | 12 | def remove_dot_segments(path): 13 | result_segments = [] 14 | 15 | while path: 16 | if path.startswith('../'): 17 | path = path[3:] 18 | elif path.startswith('./'): 19 | path = path[2:] 20 | elif path.startswith('/./'): 21 | path = path[2:] 22 | elif path == '/.': 23 | path = '/' 24 | elif path.startswith('/../'): 25 | path = path[3:] 26 | if result_segments: 27 | result_segments.pop() 28 | elif path == '/..': 29 | path = '/' 30 | if result_segments: 31 | result_segments.pop() 32 | elif path == '..' or path == '.': 33 | path = '' 34 | else: 35 | i = 0 36 | if path[0] == '/': 37 | i = 1 38 | i = path.find('/', i) 39 | if i == -1: 40 | i = len(path) 41 | result_segments.append(path[:i]) 42 | path = path[i:] 43 | 44 | return ''.join(result_segments) 45 | 46 | 47 | GEN_DELIMS = ":" + "/" + "?" + "#" + "[" + "]" + "@" 48 | SUB_DELIMS = "!" + "$" + "&" + "'" + "(" + ")" + "*" + "+" + "," + ";" + "=" 49 | RESERVED = GEN_DELIMS + SUB_DELIMS 50 | UNRESERVED = string.ascii_letters + string.digits + "-" + "." + "_" + "~" 51 | # Allow "%" as percent encoding character 52 | PERCENT_ENCODING_CHARACTER = "%" 53 | 54 | 55 | def _check_disallowed_characters(uri_part, part_name): 56 | # Roughly check the allowed characters. The check in not strict according to URI ABNF, but good enough. 57 | # Also allow "%" for percent encoding. 58 | if set(uri_part).difference(set(UNRESERVED + RESERVED + PERCENT_ENCODING_CHARACTER)): 59 | raise ValueError('Illegal characters in URI {}: {}'.format(part_name, uri_part)) 60 | 61 | 62 | def urinorm(uri): 63 | """Return normalized URI. 64 | 65 | Normalization if performed according to RFC 3986, section 6 https://tools.ietf.org/html/rfc3986#section-6. 66 | Supported URIs are URLs and OpenID realm URIs. 67 | 68 | @type uri: six.text_type, six.binary_type deprecated 69 | @rtype: six.text_type 70 | @raise ValueError: If URI is invalid. 71 | """ 72 | uri = string_to_text(uri, "Binary input for urinorm is deprecated. Use text input instead.") 73 | 74 | split_uri = urlsplit(uri) 75 | 76 | # Normalize scheme 77 | scheme = split_uri.scheme.lower() 78 | if scheme not in ('http', 'https'): 79 | raise ValueError('Not an absolute HTTP or HTTPS URI: {!r}'.format(uri)) 80 | 81 | # Normalize netloc 82 | if not split_uri.netloc: 83 | raise ValueError('Not an absolute URI: {!r}'.format(uri)) 84 | 85 | hostname = split_uri.hostname 86 | if hostname is None: 87 | hostname = '' 88 | else: 89 | hostname = hostname.lower() 90 | # Unquote percent encoded characters 91 | hostname = unquote(hostname) 92 | # Quote IDN domain names 93 | try: 94 | # hostname: str --[idna]--> bytes --[utf-8]--> str 95 | hostname = hostname.encode('idna').decode('utf-8') 96 | except ValueError as error: 97 | raise ValueError('Invalid hostname {!r}: {}'.format(hostname, error)) 98 | _check_disallowed_characters(hostname, 'hostname') 99 | 100 | try: 101 | port = split_uri.port 102 | except ValueError as error: 103 | raise ValueError('Invalid port in {!r}: {}'.format(split_uri.netloc, error)) 104 | if port is None: 105 | port = '' 106 | elif (scheme == 'http' and port == 80) or (scheme == 'https' and port == 443): 107 | port = '' 108 | 109 | netloc = hostname 110 | if port: 111 | netloc = netloc + ':' + six.text_type(port) 112 | userinfo_chunks = [i for i in (split_uri.username, split_uri.password) if i is not None] 113 | if userinfo_chunks: 114 | userinfo = ':'.join(userinfo_chunks) 115 | _check_disallowed_characters(userinfo, 'userinfo') 116 | netloc = userinfo + '@' + netloc 117 | 118 | # Normalize path 119 | path = split_uri.path 120 | # Unquote and quote - this normalizes the percent encoding 121 | 122 | # This is hackish. `unquote` and `quote` requires `str` in both py27 and py3+. 123 | if isinstance(path, str): 124 | # Python 3 branch 125 | path = quote(unquote(path), safe='/' + SUB_DELIMS) 126 | else: 127 | # Python 2 branch 128 | path = quote(unquote(path.encode('utf-8')), safe=('/' + SUB_DELIMS).encode('utf-8')).decode('utf-8') 129 | 130 | path = remove_dot_segments(path) 131 | if not path: 132 | path = '/' 133 | _check_disallowed_characters(path, 'path') 134 | 135 | # Normalize query. On Python 2, `urlencode` without `doseq=True` 136 | # requires values to be convertible to native strings using `str()`. 137 | if isinstance(split_uri.query, str): 138 | # Python 3 branch 139 | data = parse_qsl(split_uri.query) 140 | else: 141 | # Python 2 branch 142 | data = parse_qsl(split_uri.query.encode('utf-8')) 143 | query = urlencode(data) 144 | _check_disallowed_characters(query, 'query') 145 | 146 | # Normalize fragment 147 | fragment = unquote(split_uri.fragment) 148 | _check_disallowed_characters(fragment, 'fragment') 149 | 150 | return urlunsplit((scheme, netloc, path, query, fragment)) 151 | -------------------------------------------------------------------------------- /openid/yadis/accept.py: -------------------------------------------------------------------------------- 1 | """Functions for generating and parsing HTTP Accept: headers for supporting server-directed content negotiation.""" 2 | from __future__ import unicode_literals 3 | 4 | from operator import itemgetter 5 | 6 | import six 7 | 8 | from openid.oidutil import string_to_text 9 | 10 | 11 | def generateAcceptHeader(*elements): 12 | """Generate an accept header value 13 | 14 | [six.text_type or (six.text_type, float)] -> six.text_type 15 | """ 16 | parts = [] 17 | for element in elements: 18 | if isinstance(element, six.string_types): 19 | qs = "1.0" 20 | mtype = string_to_text(element, 21 | "Binary values for generateAcceptHeader are deprecated. Use text input instead.") 22 | else: 23 | mtype, q = element 24 | mtype = string_to_text(mtype, 25 | "Binary values for generateAcceptHeader are deprecated. Use text input instead.") 26 | q = float(q) 27 | if q > 1 or q <= 0: 28 | raise ValueError('Invalid preference factor: %r' % q) 29 | 30 | qs = '%0.1f' % (q,) 31 | 32 | parts.append((qs, mtype)) 33 | 34 | parts.sort() 35 | chunks = [] 36 | for q, mtype in parts: 37 | if q == '1.0': 38 | chunks.append(mtype) 39 | else: 40 | chunks.append('%s; q=%s' % (mtype, q)) 41 | 42 | return ', '.join(chunks) 43 | 44 | 45 | def parseAcceptHeader(value): 46 | """Parse an accept header, ignoring any accept-extensions 47 | 48 | returns a list of tuples containing main MIME type, MIME subtype, 49 | and quality markdown. 50 | 51 | six.text_type -> [(six.text_type, six.text_type, float)] 52 | """ 53 | value = string_to_text(value, "Binary values for parseAcceptHeader are deprecated. Use text input instead.") 54 | chunks = [chunk.strip() for chunk in value.split(',')] 55 | accept = [] 56 | for chunk in chunks: 57 | parts = [s.strip() for s in chunk.split(';')] 58 | 59 | mtype = parts.pop(0) 60 | if '/' not in mtype: 61 | # This is not a MIME type, so ignore the bad data 62 | continue 63 | 64 | main, sub = mtype.split('/', 1) 65 | 66 | for ext in parts: 67 | if '=' in ext: 68 | k, v = ext.split('=', 1) 69 | if k == 'q': 70 | try: 71 | q = float(v) 72 | break 73 | except ValueError: 74 | # Ignore poorly formed q-values 75 | pass 76 | else: 77 | q = 1.0 78 | 79 | accept.append((main, sub, q)) 80 | 81 | # Sort in order q, main, sub 82 | return sorted(accept, key=itemgetter(2, 0, 1), reverse=True) 83 | 84 | 85 | def matchTypes(accept_types, have_types): 86 | """Given the result of parsing an Accept: header, and the 87 | available MIME types, return the acceptable types with their 88 | quality markdowns. 89 | 90 | For example: 91 | 92 | >>> acceptable = parseAcceptHeader('text/html, text/plain; q=0.5') 93 | >>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg']) 94 | [('text/html', 1.0), ('text/plain', 0.5)] 95 | 96 | 97 | Type signature: ([(six.text_type, six.text_type, float)], [six.text_type]) -> [(six.text_type, float)] 98 | """ 99 | if not accept_types: 100 | # Accept all of them 101 | default = 1 102 | else: 103 | default = 0 104 | 105 | match_main = {} 106 | match_sub = {} 107 | for (main, sub, qvalue) in accept_types: 108 | main = string_to_text(main, "Binary values for matchTypes accept_types are deprecated. Use text input instead.") 109 | sub = string_to_text(sub, "Binary values for matchTypes accept_types are deprecated. Use text input instead.") 110 | if main == '*': 111 | default = max(default, qvalue) 112 | continue 113 | elif sub == '*': 114 | match_main[main] = max(match_main.get(main, 0), qvalue) 115 | else: 116 | match_sub[(main, sub)] = max(match_sub.get((main, sub), 0), qvalue) 117 | 118 | accepted_list = [] 119 | order_maintainer = 0 120 | for mtype in have_types: 121 | mtype = string_to_text(mtype, "Binary values for matchTypes have_types are deprecated. Use text input instead.") 122 | main, sub = mtype.split('/') 123 | if (main, sub) in match_sub: 124 | quality = match_sub[(main, sub)] 125 | else: 126 | quality = match_main.get(main, default) 127 | 128 | if quality: 129 | accepted_list.append((1 - quality, order_maintainer, quality, mtype)) 130 | order_maintainer += 1 131 | 132 | accepted_list.sort() 133 | return [(match, q) for (_, _, q, match) in accepted_list] 134 | 135 | 136 | def getAcceptable(accept_header, have_types): 137 | """Parse the accept header and return a list of available types in 138 | preferred order. If a type is unacceptable, it will not be in the 139 | resulting list. 140 | 141 | This is a convenience wrapper around matchTypes and 142 | parseAcceptHeader. 143 | 144 | (six.text_type, [six.text_type]) -> [six.text_type] 145 | """ 146 | accept_header = string_to_text( 147 | accept_header, "Binary values for getAcceptable accept_header are deprecated. Use text input instead.") 148 | accepted = parseAcceptHeader(accept_header) 149 | preferred = matchTypes(accepted, have_types) 150 | return [mtype for (mtype, _) in preferred] 151 | -------------------------------------------------------------------------------- /openid/test/test_parsehtml.py: -------------------------------------------------------------------------------- 1 | """Tests for `openid.yadis.parsehtml` module.""" 2 | from __future__ import unicode_literals 3 | 4 | import unittest 5 | 6 | from mock import sentinel 7 | from six import StringIO 8 | 9 | from openid.yadis.parsehtml import MetaNotFound, findHTMLMeta, xpath_lower_case 10 | 11 | 12 | class TestXpathLowerCase(unittest.TestCase): 13 | """Test `xpath_lower_case` function.""" 14 | 15 | def test_lower_case(self): 16 | self.assertEqual(xpath_lower_case(sentinel.context, ['CaMeLcAsE']), ['camelcase']) 17 | 18 | 19 | class TestFindHTMLMeta(unittest.TestCase): 20 | """Test `findHTMLMeta` function.""" 21 | 22 | def test_html(self): 23 | buff = StringIO('') 24 | self.assertEqual(findHTMLMeta(buff), 'found') 25 | 26 | def test_xhtml(self): 27 | buff = StringIO('') 28 | self.assertEqual(findHTMLMeta(buff), 'found') 29 | 30 | def test_case_insensitive_header_name(self): 31 | buff = StringIO('') 32 | self.assertEqual(findHTMLMeta(buff), 'found') 33 | 34 | def test_missing_end_tags(self): 35 | buff = StringIO('') 36 | self.assertEqual(findHTMLMeta(buff), 'found') 37 | 38 | def test_missing_html_header(self): 39 | buff = StringIO('') 40 | self.assertEqual(findHTMLMeta(buff), 'found') 41 | 42 | def test_missing_head_tag(self): 43 | buff = StringIO('') 44 | self.assertEqual(findHTMLMeta(buff), 'found') 45 | 46 | def test_top_level_bogus(self): 47 | buff = StringIO('') 48 | self.assertEqual(findHTMLMeta(buff), 'found') 49 | 50 | def test_missing_html_tag(self): 51 | buff = StringIO('') 52 | self.assertEqual(findHTMLMeta(buff), 'found') 53 | 54 | def test_javascript_in_head(self): 55 | buff = StringIO('' 56 | '') 57 | self.assertEqual(findHTMLMeta(buff), 'found') 58 | 59 | def test_multiple_headers(self): 60 | buff = StringIO('' 61 | '' 62 | '') 63 | self.assertEqual(findHTMLMeta(buff), 'found') 64 | 65 | def test_standard_entity(self): 66 | buff = StringIO('') 67 | self.assertEqual(findHTMLMeta(buff), '&') 68 | 69 | def test_hex_entity(self): 70 | buff = StringIO('') 71 | self.assertEqual(findHTMLMeta(buff), 'found') 72 | 73 | def test_decimal_entity(self): 74 | buff = StringIO('') 75 | self.assertEqual(findHTMLMeta(buff), 'found') 76 | 77 | def test_empty_string(self): 78 | buff = StringIO('') 79 | self.assertEqual(findHTMLMeta(buff), '') 80 | 81 | def test_empty_input(self): 82 | buff = StringIO('') 83 | self.assertRaises(MetaNotFound, findHTMLMeta, buff) 84 | 85 | def test_invalid_html(self): 86 | buff = StringIO('') 87 | self.assertRaises(MetaNotFound, findHTMLMeta, buff) 88 | 89 | def test_meta_in_body(self): 90 | buff = StringIO('') 91 | self.assertRaises(MetaNotFound, findHTMLMeta, buff) 92 | 93 | def test_no_content(self): 94 | buff = StringIO('') 95 | self.assertRaises(MetaNotFound, findHTMLMeta, buff) 96 | 97 | def test_commented_header(self): 98 | buff = StringIO('' 99 | '' 100 | '') 101 | self.assertRaises(MetaNotFound, findHTMLMeta, buff) 102 | 103 | def test_no_yadis_header(self): 104 | buff = StringIO("A boring document" 105 | "

A boring document

There's really nothing interesting about this

" 106 | "") 107 | self.assertRaises(MetaNotFound, findHTMLMeta, buff) 108 | 109 | def test_unclosed_tag(self): 110 | # script tag not closed 111 | buff = StringIO(' 33 | 34 | 35 | """ % (title, form) 36 | 37 | 38 | def log(message, level=0): 39 | """Handle a log message from the OpenID library. 40 | 41 | This is a legacy function which redirects to logging.error. 42 | The logging module should be used instead of this 43 | 44 | @param message: A string containing a debugging message from the 45 | OpenID library 46 | @type message: six.text_type, six.binary_type is deprecated 47 | 48 | @param level: The severity of the log message. This parameter is 49 | currently unused, but in the future, the library may indicate 50 | more important information with a higher level value. 51 | @type level: int or None 52 | 53 | @returns: Nothing. 54 | """ 55 | message = string_to_text(message, "Binary values for log are deprecated. Use text input instead.") 56 | 57 | logging.error("This is a legacy log message, please use the logging module. Message: %s", message) 58 | 59 | 60 | def appendArgs(url, args): 61 | """Append query arguments to a HTTP(s) URL. If the URL already has 62 | query arguemtns, these arguments will be added, and the existing 63 | arguments will be preserved. Duplicate arguments will not be 64 | detected or collapsed (both will appear in the output). 65 | 66 | @param url: The url to which the arguments will be appended 67 | @type url: six.text_type, six.binary_type is deprecated 68 | 69 | @param args: The query arguments to add to the URL. If a 70 | dictionary is passed, the items will be sorted before 71 | appending them to the URL. If a sequence of pairs is passed, 72 | the order of the sequence will be preserved. 73 | @type args: Union[Dict[six.text_type, six.text_type], List[Tuple[six.text_type, six.text_type]]], 74 | six.binary_type is deprecated 75 | 76 | @returns: The URL with the parameters added 77 | @rtype: six.text_type 78 | """ 79 | url = string_to_text(url, "Binary values for appendArgs are deprecated. Use text input instead.") 80 | 81 | if hasattr(args, 'items'): 82 | args = sorted(args.items()) 83 | else: 84 | args = list(args) 85 | 86 | if len(args) == 0: 87 | return url 88 | 89 | if '?' in url: 90 | sep = '&' 91 | else: 92 | sep = '?' 93 | 94 | i = 0 95 | for k, v in args: 96 | k = string_to_text(k, "Binary values for appendArgs are deprecated. Use text input instead.") 97 | v = string_to_text(v, "Binary values for appendArgs are deprecated. Use text input instead.") 98 | args[i] = (k.encode('utf-8'), v.encode('utf-8')) 99 | i += 1 100 | 101 | encoded_args = urlencode(args) 102 | # `urlencode` returns `str` in both py27 and py3+. We need to convert it to six.text_type. 103 | if not isinstance(encoded_args, six.text_type): 104 | encoded_args = encoded_args.decode('utf-8') 105 | return '%s%s%s' % (url, sep, encoded_args) 106 | 107 | 108 | def toBase64(s): 109 | """Return string s as base64, omitting newlines. 110 | 111 | @type s: six.binary_type 112 | @rtype six.text_type 113 | """ 114 | return binascii.b2a_base64(s)[:-1].decode('utf-8') 115 | 116 | 117 | def fromBase64(s): 118 | """Return binary data from base64 encoded string. 119 | 120 | @type s: six.text_type, six.binary_type deprecated. 121 | @rtype six.binary_type 122 | """ 123 | s = string_to_text(s, "Binary values for s are deprecated. Use text input instead.") 124 | try: 125 | return binascii.a2b_base64(s) 126 | except binascii.Error as why: 127 | # Convert to a common exception type 128 | raise ValueError(six.text_type(why)) 129 | 130 | 131 | class Symbol(object): 132 | """This class implements an object that compares equal to others 133 | of the same type that have the same name. These are distict from 134 | string objects. 135 | """ 136 | 137 | def __init__(self, name): 138 | self.name = name 139 | 140 | def __eq__(self, other): 141 | return type(self) == type(other) and self.name == other.name 142 | 143 | def __ne__(self, other): 144 | return not (self == other) 145 | 146 | def __hash__(self): 147 | return hash((self.__class__, self.name)) 148 | 149 | def __repr__(self): 150 | return '' % (self.name,) 151 | 152 | 153 | def string_to_text(value, deprecate_msg): 154 | """ 155 | Return input string coverted to text string. 156 | 157 | If input is text, it is returned as is. 158 | If input is binary, it is decoded using UTF-8 to text. 159 | """ 160 | assert isinstance(value, (six.text_type, six.binary_type)) 161 | if isinstance(value, six.binary_type): 162 | warnings.warn(deprecate_msg, DeprecationWarning) 163 | value = value.decode('utf-8') 164 | return value 165 | 166 | 167 | def force_text(value): 168 | """ 169 | Return a text object representing value in UTF-8 encoding. 170 | """ 171 | if isinstance(value, six.text_type): 172 | # It's already a text, just return it. 173 | return value 174 | elif isinstance(value, bytes): 175 | # It's a byte string, decode it. 176 | return value.decode('utf-8') 177 | else: 178 | # It's not a string, convert it. 179 | return six.text_type(value) 180 | --------------------------------------------------------------------------------