├── .coveragerc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NEWS.md ├── README.md ├── admin ├── build_discover_data.py ├── get_tlds.py ├── next_version.py └── patch_version.py ├── background-associations.txt ├── contrib ├── associate ├── openid-parse └── upgrade-store-1.1-to-2.0 ├── dev-requirements.txt ├── examples ├── README ├── __init__.py ├── consumer.py ├── discover ├── djopenid │ ├── README │ ├── __init__.py │ ├── consumer │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ ├── server │ │ ├── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── settings.py │ ├── templates │ │ ├── consumer │ │ │ ├── index.html │ │ │ └── request_form.html │ │ ├── index.html │ │ ├── server │ │ │ ├── endpoint.html │ │ │ ├── idPage.html │ │ │ ├── index.html │ │ │ ├── pape_request_info.html │ │ │ └── trust.html │ │ └── xrds.xml │ ├── urls.py │ ├── util.py │ └── views.py └── server.py ├── openid ├── __init__.py ├── association.py ├── codecutil.py ├── consumer │ ├── __init__.py │ ├── consumer.py │ ├── discover.py │ └── html_parse.py ├── cryptutil.py ├── dh.py ├── extension.py ├── extensions │ ├── __init__.py │ ├── ax.py │ ├── draft │ │ ├── __init__.py │ │ ├── pape2.py │ │ └── pape5.py │ └── sreg.py ├── fetchers.py ├── kvform.py ├── message.py ├── oidutil.py ├── server │ ├── __init__.py │ ├── server.py │ └── trustroot.py ├── sreg.py ├── store │ ├── __init__.py │ ├── filestore.py │ ├── interface.py │ ├── memstore.py │ ├── nonce.py │ └── sqlstore.py ├── test │ ├── __init__.py │ ├── cryptutil.py │ ├── data │ │ ├── accept.txt │ │ ├── example-xrds.xml │ │ ├── openid-1.2-consumer-sqlitestore.db │ │ ├── test1-discover.txt │ │ ├── test1-parsehtml.txt │ │ ├── test_discover │ │ │ ├── openid.html │ │ │ ├── openid2.html │ │ │ ├── openid2_xrds.xml │ │ │ ├── openid2_xrds_no_local_id.xml │ │ │ ├── openid_1_and_2.html │ │ │ ├── openid_1_and_2_xrds.xml │ │ │ ├── openid_1_and_2_xrds_bad_delegate.xml │ │ │ ├── openid_and_yadis.html │ │ │ ├── openid_no_delegate.html │ │ │ ├── unicode.html │ │ │ ├── unicode3.html │ │ │ ├── yadis_0entries.xml │ │ │ ├── yadis_2_bad_local_id.xml │ │ │ ├── yadis_2entries_delegate.xml │ │ │ ├── yadis_2entries_idp.xml │ │ │ ├── yadis_another_delegate.xml │ │ │ ├── yadis_idp.xml │ │ │ ├── yadis_idp_delegate.xml │ │ │ └── yadis_no_delegate.xml │ │ ├── test_etxrd │ │ │ ├── README │ │ │ ├── delegated-20060809-r1.xrds │ │ │ ├── delegated-20060809-r2.xrds │ │ │ ├── delegated-20060809.xrds │ │ │ ├── no-xrd.xml │ │ │ ├── not-xrds.xml │ │ │ ├── prefixsometimes.xrds │ │ │ ├── ref.xrds │ │ │ ├── sometimesprefix.xrds │ │ │ ├── spoof1.xrds │ │ │ ├── spoof2.xrds │ │ │ ├── spoof3.xrds │ │ │ ├── status222.xrds │ │ │ ├── subsegments.xrds │ │ │ └── valid-populated-xrds.xml │ │ └── trustroot.txt │ ├── datadriven.py │ ├── dh.py │ ├── dhpriv │ ├── discoverdata.py │ ├── kvform.py │ ├── linkparse.py │ ├── linkparse.txt │ ├── n2b64 │ ├── oidutil.py │ ├── storetest.py │ ├── support.py │ ├── test_accept.py │ ├── test_association.py │ ├── test_association_response.py │ ├── test_auth_request.py │ ├── test_ax.py │ ├── test_codecutil.py │ ├── test_consumer.py │ ├── test_discover.py │ ├── test_etxrd.py │ ├── test_examples.py │ ├── test_extension.py │ ├── test_fetchers.py │ ├── test_htmldiscover.py │ ├── test_message.py │ ├── test_negotiation.py │ ├── test_nonce.py │ ├── test_openidyadis.py │ ├── test_pape.py │ ├── test_pape_draft2.py │ ├── test_pape_draft5.py │ ├── test_parsehtml.py │ ├── test_rpverify.py │ ├── test_server.py │ ├── test_services.py │ ├── test_sreg.py │ ├── test_symbol.py │ ├── test_urinorm.py │ ├── test_verifydisco.py │ ├── test_xri.py │ ├── test_xrires.py │ ├── test_yadis_discover.py │ ├── trustroot.py │ └── urinorm.txt ├── urinorm.py └── yadis │ ├── __init__.py │ ├── accept.py │ ├── constants.py │ ├── discover.py │ ├── etxrd.py │ ├── filters.py │ ├── manager.py │ ├── parsehtml.py │ ├── services.py │ ├── xri.py │ └── xrires.py ├── pylintrc ├── requirements.txt ├── setup.cfg └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | examples 4 | openid 5 | omit = 6 | openid/test/* 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | # We need to specify 20.04 to maintain compatibility with older Python versions :-( 14 | runs-on: ubuntu-20.04 15 | services: 16 | postgres: 17 | image: postgres:latest 18 | env: 19 | POSTGRES_USER: postgres 20 | POSTGRES_PASSWORD: password 21 | ports: 22 | - 5432/tcp 23 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 24 | mysql: 25 | image: mysql:latest 26 | env: 27 | MYSQL_USER: mysql 28 | MYSQL_PASSWORD: password 29 | MYSQL_RANDOM_ROOT_PASSWORD: true 30 | ports: 31 | - 3306/tcp 32 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 33 | env: 34 | TEST_MYSQL_HOST: mysql 35 | TEST_MYSQL_USER: mysql 36 | TEST_MYSQL_PASSWORD: password 37 | TEST_POSTGRES_HOST: postgres 38 | TEST_POSTGRES_USER: postgres 39 | TEST_POSTGRES_PASSWORD: password 40 | continue-on-error: ${{ contains(matrix.python-version, 'pypy') || contains(matrix.python-version, 'dev') }} 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | python-version: 45 | - "3.5" 46 | - "3.6" 47 | - "3.7" 48 | - "3.8" 49 | - "3.9" 50 | - "3.10" 51 | - "3.11" 52 | - "3.12" 53 | - "3.13" 54 | - "pypy3.6" 55 | - "pypy3.7" 56 | - "pypy3.8" 57 | - "pypy3.9" 58 | - "pypy3.10" 59 | steps: 60 | - uses: awalsh128/cache-apt-pkgs-action@latest 61 | with: 62 | packages: libcurl4-openssl-dev 63 | version: 1.0 64 | - uses: actions/checkout@v4 65 | - uses: actions/setup-python@v5 66 | with: 67 | python-version: ${{ matrix.python-version }} 68 | env: 69 | # Workaround from https://github.com/actions/setup-python/issues/866 70 | PIP_TRUSTED_HOST: ${{ contains(matrix.python-version, '3.5') && 'pypi.python.org pypi.org files.pythonhosted.org' || 'none'}} 71 | cache: "pip" 72 | - run: > 73 | pip install wheel 74 | && pip install -r dev-requirements.txt 75 | && pip install . 76 | env: 77 | # Workaround from https://github.com/actions/setup-python/issues/866 78 | PIP_TRUSTED_HOST: ${{ contains(matrix.python-version, '3.5') && 'pypi.python.org pypi.org files.pythonhosted.org' || 'none'}} 79 | - run: coverage run -m unittest openid.test.test_suite 80 | - run: coverage report 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | __pycache__/ 3 | *.pyc 4 | *.swp 5 | 6 | .tox 7 | htmlcov 8 | .coverage 9 | .coverage.* 10 | 11 | MANIFEST 12 | build/ 13 | dist/ 14 | sdist/ 15 | *.egg-info/ 16 | 17 | .venv 18 | .envrc 19 | .direnv 20 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2 | As of 3.0.0: 3 | 4 | * API changes 5 | * SQLStore implementations no longer create or use a 'settings' 6 | table 7 | * SRegResponse.fromSuccessResponse returns None when no signed 8 | arguments were found 9 | * Added functions to generate request/response HTML forms with 10 | auto-submission javascript 11 | * Consumer (relying party) API: AuthRequest.htmlMarkup 12 | * Server API: server.OpenIDResponse.toHTML 13 | * PAPE (Provider Authentication Policy Extension) module 14 | * Updated extension for specification draft 2 15 | * Request.fromSuccessResponse returns None if PAPE response 16 | arguments were not signed 17 | 18 | * New features 19 | * Demo server now supports OP-driven identifier selection 20 | * Demo consumer now has a "stateless" option 21 | * Fetchers now only read/request first megabyte of response 22 | 23 | * Bug fixes 24 | * NOT NULL constraints were added to SQLStore tables where 25 | appropriate 26 | * message.fromPostArgs: use query.items() instead of iteritems(), 27 | fixes #161 (Affects Django users) 28 | * check_authentication requests: copy entire response, not just 29 | signed fields. Fixes missing namespace in check_authentication 30 | requests 31 | * Consumer._verifyDiscoveryResults: fall back to OpenID 1.0 type if 32 | 1.1 endpoint cannot be found; fixes discovery verification bug for 33 | certain OpenID 1 identifiers 34 | * SQLStore._execSQL: convert unicode arguments to str to avoid 35 | postgresql api bug with unicode objects (Thanks to Marek Kuziel.) 36 | * MySQLStore: Use ENGINE instead of TYPE when creating tables 37 | * server.OpenIDResponse.toFormMarkup: Use return_to from the 38 | request, not the response fields (Not all responses (i.e. cancel, 39 | setup_needed) include a return_to field.) 40 | * server.AssociationRequest.answer: include session_type in 41 | no-encryption assoc responses 42 | * OpenIDServiceEndpoint.getDisplayIdentifier: Don't include the 43 | fragment in display identifiers. 44 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE NOTICE CHANGELOG MANIFEST.in NEWS.md background-associations.txt 2 | graft admin 3 | graft contrib 4 | recursive-include examples README.md discover *.py *.html *.xml 5 | recursive-include openid/test *.txt dhpriv n2b64 *.py 6 | recursive-include openid/test/data * 7 | recursive-include doc *.css *.html 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean 2 | 3 | # Helper function to complete multiple steps in a release -- compute the next 4 | # version, patch the code to embed it, and tag it in Git 5 | define release 6 | $(eval NEXTVER := $(shell python admin/next_version.py --$(1))) 7 | python admin/patch_version.py ${NEXTVER} 8 | git commit -m \"Version $(NEXTVER)\" -- openid/__init__.py # && 9 | git tag "v$(NEXTVER)" -m \"Version $(NEXTVER)\" 10 | endef 11 | 12 | clean: 13 | find . -name '*.pyc' -exec rm -f {} + 14 | find . -name '*.pyo' -exec rm -f {} + 15 | find . -name '*~' -exec rm -f {} + 16 | 17 | upload: 18 | rm -rf dist/* 19 | python setup.py clean sdist bdist_wheel 20 | twine upload dist/* 21 | 22 | test: 23 | coverage run -m unittest openid.test.test_suite 24 | 25 | release-patch: clean test 26 | @$(call release,patch) 27 | 28 | release-minor: clean test 29 | @$(call release,minor) 30 | 31 | release-major: clean test 32 | @$(call release,major) 33 | 34 | push-tags: 35 | git push --tags origin HEAD:master 36 | 37 | publish: push-tags upload 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _NOTE_: This started out as a fork of the Python OpenID library, with changes 2 | to make it Python 3 compatible. It's now a port of that library, including 3 | cleanups and updates to the code in general. 4 | 5 | [![Build Status](https://github.com/necaris/python3-openid/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/necaris/python3-openid/actions?query=branch%3Amain) 6 | [![Coverage Status](https://coveralls.io/repos/necaris/python3-openid/badge.svg?branch=master&service=github)](https://coveralls.io/github/necaris/python3-openid?branch=master) 7 | 8 | # requirements 9 | 10 | - Python 3.5+ (tested on CPython 3.5-3.8, and PyPy3 (although some tests may fail on PyPy)) 11 | 12 | # installation 13 | 14 | The recommended way is to install from PyPI with `pip`: 15 | 16 | pip install python3-openid 17 | 18 | Alternatively, you can run the following command from a source checkout: 19 | 20 | python setup.py install 21 | 22 | If you want to use MySQL or PostgreSQL storage options, be sure to install 23 | the relevant "extra": 24 | 25 | pip install python3-openid[mysql] 26 | 27 | # getting started 28 | 29 | The library should follow the existing `python-openid` API as closely as possible. 30 | 31 | _NOTE_: documentation will be auto-generated as soon as I can figure out how to 32 | update the documentation tools. 33 | 34 | _NOTE_: 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 | # logging 39 | 40 | This library offers a logging hook that will record unexpected 41 | conditions that occur in library code. If a condition is recoverable, 42 | the library will recover and issue a log message. If it is not 43 | recoverable, the library will raise an exception. See the 44 | documentation for the `openid.oidutil` module for more on the logging 45 | hook. 46 | 47 | # documentation 48 | 49 | The documentation in this library is in Epydoc format, which is 50 | detailed at: 51 | 52 | http://epydoc.sourceforge.net/ 53 | 54 | # contact 55 | 56 | Bug reports, suggestions, and feature requests are [very welcome](../../issues)! 57 | 58 | There are also the `#python-openid` and `#openid` channels on FreeNode IRC. 59 | 60 | # contributors 61 | 62 | - @necaris 63 | - @moreati 64 | - @vstoykov 65 | - @earthday 66 | - @bkmgit 67 | -------------------------------------------------------------------------------- /admin/build_discover_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Build a set of YADIS identity URL / service discovery files in 4 | the format for Apache mod_asis -- simple text files containing their 5 | own HTTP headers. 6 | 7 | These can then be used as a basis for testing. 8 | """ 9 | 10 | import sys 11 | import os.path 12 | import urllib.parse 13 | 14 | from openid.test import discoverdata 15 | 16 | manifest_header = """\ 17 | # This file contains test cases for doing YADIS identity URL and 18 | # service discovery. For each case, there are three URLs. The first 19 | # URL is the user input. The second is the identity URL and the third 20 | # is the URL from which the XRDS document should be read. 21 | # 22 | # The file format is as follows: 23 | # User URL Identity URL XRDS URL 24 | # 25 | # blank lines and lines starting with # should be ignored. 26 | # 27 | # To use this test: 28 | # 29 | # 1. Run your discovery routine on the User URL. 30 | # 31 | # 2. Compare the identity URL returned by the discovery routine to the 32 | # identity URL on that line of the file. It must be an EXACT match. 33 | # 34 | # 3. Do a regular HTTP GET on the XRDS URL. Compare the content that 35 | # was returned by your discovery routine with the content returned 36 | # from that URL. It should also be an exact match. 37 | 38 | """ 39 | 40 | 41 | def buildDiscover(base_url, out_dir): 42 | """ 43 | Convert all files in a directory to apache mod_asis files in 44 | another directory. 45 | """ 46 | test_data = discoverdata.readTests(discoverdata.default_test_file) 47 | 48 | def writeTestFile(test_name): 49 | """Helper to generate an output data file for a given test name.""" 50 | template = test_data[test_name] 51 | 52 | data = discoverdata.fillTemplate(test_name, template, base_url, 53 | discoverdata.example_xrds) 54 | 55 | out_file_name = os.path.join(out_dir, test_name) 56 | with open(out_file_name, 'w', encoding="utf-8") as out_file: 57 | out_file.write(data) 58 | 59 | manifest = [manifest_header] 60 | for success, input_name, id_name, result_name in discoverdata.testlist: 61 | if not success: 62 | continue 63 | writeTestFile(input_name) 64 | 65 | input_url = urllib.parse.urljoin(base_url, input_name) 66 | id_url = urllib.parse.urljoin(base_url, id_name) 67 | result_url = urllib.parse.urljoin(base_url, result_name) 68 | 69 | manifest.append('\t'.join((input_url, id_url, result_url))) 70 | manifest.append('\n') 71 | 72 | manifest_file_name = os.path.join(out_dir, 'manifest.txt') 73 | with open(manifest_file_name, 'w', encoding="utf-8") as manifest_file: 74 | for chunk in manifest: 75 | manifest_file.write(chunk) 76 | 77 | 78 | if __name__ == '__main__': 79 | buildDiscover(*sys.argv[1:]) 80 | -------------------------------------------------------------------------------- /admin/get_tlds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Fetch the current TLD list from the IANA Web site, parse it, and print 4 | an expression suitable for direct insertion into each library's trust 5 | root validation module. 6 | 7 | Usage: 8 | python gettlds.py (php|python|ruby) 9 | 10 | Then cut-n-paste. 11 | """ 12 | 13 | import urllib.request 14 | import sys 15 | 16 | LANGS = { 17 | 'php': ( 18 | r"'/\.(", # prefix 19 | "'", # line prefix 20 | "|", # separator 21 | "|' .", # line suffix 22 | r")\.?$/'" # suffix 23 | ), 24 | 'python': ("['", "'", "', '", "',", "']"), 25 | 'ruby': ("%w'", "", " ", "", "'"), 26 | } 27 | 28 | if __name__ == '__main__': 29 | 30 | lang = sys.argv[1] 31 | prefix, line_prefix, separator, line_suffix, suffix = LANGS[lang] 32 | 33 | iana_url = 'http://data.iana.org/TLD/tlds-alpha-by-domain.txt' 34 | 35 | with urllib.request.urlopen(iana_url) as iana_resource: 36 | tlds = [] 37 | output_line = "" # initialize a line of output 38 | 39 | for input_line in iana_resource: 40 | if input_line.startswith(b'#'): # skip comments 41 | continue 42 | 43 | tld = input_line.decode("utf-8").strip().lower() 44 | nxt_output_line = output_line + prefix + tld # update current line 45 | 46 | if len(nxt_output_line) > 60: 47 | # Long enough -- print it and reinitialize to only hold the 48 | # most recent TLD 49 | print(output_line + line_suffix) 50 | output_line = line_prefix + tld 51 | else: 52 | # Not long enough, so update it to the concatenated version 53 | output_line = nxt_output_line 54 | 55 | prefix = separator 56 | 57 | # Print the final line of remaining output 58 | print(output_line + suffix) 59 | -------------------------------------------------------------------------------- /admin/next_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Compute the next release version of the library, using `--major`, `--minor`, 4 | or `--patch` arguments to determine the level at which the version is to be 5 | incremented. 6 | """ 7 | import sys 8 | from os.path import abspath, join, dirname 9 | 10 | if __name__ == '__main__': 11 | sys.path.append(abspath(join(dirname(__file__), '..'))) 12 | 13 | import openid 14 | 15 | major, minor, patch = openid.version_info 16 | pieces = None 17 | 18 | if '--major' in sys.argv: 19 | pieces = (major + 1, 0, 0) 20 | elif '--minor' in sys.argv: 21 | pieces = (major, minor + 1, 0) 22 | elif '--patch' in sys.argv: 23 | pieces = (major, minor, patch + 1) 24 | 25 | if pieces: 26 | print('.'.join(map(str, pieces)), end='') 27 | else: 28 | print('Major, minor, or patch?', file=sys.stderr) 29 | sys.exit(1) 30 | -------------------------------------------------------------------------------- /admin/patch_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Update the `version_info` embedded in the library to the given version. 4 | """ 5 | import sys 6 | from os.path import abspath, join, dirname 7 | 8 | if __name__ == '__main__': 9 | try: 10 | major, minor, patch = map(int, sys.argv[1].split('.')) 11 | except (IndexError, ValueError): 12 | print('Need version string in form MAJOR.MINOR.PATCH', file=sys.stderr) 13 | sys.exit(1) 14 | 15 | TARGET = abspath(join(dirname(__file__), '..', 'openid', '__init__.py')) 16 | 17 | with open(TARGET, 'r', encoding='utf8') as f: 18 | lines = f.readlines() 19 | for i, l in enumerate(lines): 20 | if l.startswith('version_info'): 21 | v_info = '({}, {}, {})'.format(major, minor, patch) 22 | lines[i] = 'version_info = {}\n\n'.format(v_info) 23 | break 24 | 25 | with open(TARGET, 'w', encoding='utf8') as f: 26 | f.writelines(lines) 27 | -------------------------------------------------------------------------------- /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 | logger.info("We don't need to associate with %r", endpoint.server_url) 77 | return 78 | 79 | logger.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 | logger.info('(%0.2f seconds) Associated with %r', elapsed, 85 | endpoint.server_url) 86 | consumer.store.storeAssociation(endpoint.server_url, assoc) 87 | else: 88 | logger.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 | -------------------------------------------------------------------------------- /contrib/associate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Make an OpenID Assocition request against an endpoint 3 | and print the results.""" 4 | 5 | import sys 6 | 7 | from openid.store.memstore import MemoryStore 8 | from openid.consumer import consumer 9 | from openid.consumer.discover import OpenIDServiceEndpoint 10 | 11 | from datetime import datetime 12 | 13 | 14 | def verboseAssociation(assoc): 15 | """A more verbose representation of an Association. 16 | """ 17 | d = assoc.__dict__ 18 | issued_date = datetime.fromtimestamp(assoc.issued) 19 | d['issued_iso'] = issued_date.isoformat() 20 | fmt = """ Type: %(assoc_type)s 21 | Handle: %(handle)s 22 | Issued: %(issued)s [%(issued_iso)s] 23 | Lifetime: %(lifetime)s 24 | Secret: %(secret)r 25 | """ 26 | return fmt % d 27 | 28 | 29 | def main(): 30 | if not sys.argv[1:]: 31 | print("Usage: %s ENDPOINT_URL..." % (sys.argv[0],)) 32 | for endpoint_url in sys.argv[1:]: 33 | print("Associating with", endpoint_url) 34 | 35 | # This makes it clear why j3h made AssociationManager when we 36 | # did the ruby port. We can't invoke requestAssociation 37 | # without these other trappings. 38 | store = MemoryStore() 39 | endpoint = OpenIDServiceEndpoint() 40 | endpoint.server_url = endpoint_url 41 | c = consumer.GenericConsumer(store) 42 | auth_req = c.begin(endpoint) 43 | if auth_req.assoc: 44 | print(verboseAssociation(auth_req.assoc)) 45 | else: 46 | print(" ...no association.") 47 | 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /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 | 10 | from pprint import pformat 11 | from urllib.parse import urlsplit, urlunsplit, parse_qs 12 | import re 13 | import subprocess 14 | import sys 15 | 16 | from openid import message 17 | 18 | OPENID_SORT_ORDER = ['mode', 'identity', 'claimed_id'] 19 | 20 | 21 | class NoQuery(Exception): 22 | def __init__(self, url): 23 | self.url = url 24 | 25 | def __str__(self): 26 | return "No query in url %s" % (self.url, ) 27 | 28 | 29 | def getClipboard(): 30 | xsel = subprocess.Popen(["xsel", "-o", "-b"], stdout=subprocess.PIPE) 31 | output = xsel.communicate()[0] 32 | return output 33 | 34 | 35 | def main(): 36 | source = getClipboard() 37 | urls = find_urls(source) 38 | 39 | errors = [] 40 | output = [] 41 | queries = [] 42 | 43 | queries.extend(queriesFromPostdata(source)) 44 | 45 | for url in urls: 46 | try: 47 | queries.append(queryFromURL(url)) 48 | except NoQuery as err: 49 | errors.append(err) 50 | 51 | queries.extend(queriesFromLogs(source)) 52 | 53 | for where, query in queries: 54 | output.append('at %s:\n%s' % (where, openidFromQuery(query))) 55 | 56 | if output: 57 | print('\n\n'.join(output)) 58 | elif errors: 59 | for err in errors: 60 | print(err) 61 | 62 | 63 | def queryFromURL(url): 64 | split_url = urlsplit(url) 65 | query = parse_qs(split_url[3]) 66 | 67 | if not query: 68 | raise NoQuery(url) 69 | 70 | url_without_query = urlunsplit(split_url[:3] + (None, None)) 71 | 72 | return (url_without_query, query) 73 | 74 | 75 | def openidFromQuery(query): 76 | try: 77 | msg = message.Message.fromPostArgs(unlistify(query)) 78 | s = formatOpenIDMessage(msg) 79 | except Exception as err: 80 | # XXX - side effect. 81 | sys.stderr.write(str(err)) 82 | s = pformat(query) 83 | 84 | return s 85 | 86 | 87 | def formatOpenIDMessage(msg): 88 | value_lists = {} 89 | for (ns_uri, ns_key), value in msg.args.items(): 90 | l = value_lists.setdefault(ns_uri, {}) 91 | l[ns_key] = value 92 | 93 | output = [] 94 | 95 | for ns_uri, values in value_lists.items(): 96 | ns_output = [] 97 | 98 | alias = msg.namespaces.getAlias(ns_uri) 99 | if alias is message.NULL_NAMESPACE: 100 | alias = 'openid' 101 | ns_output.append(" %s <%s>" % (alias, ns_uri)) 102 | 103 | for key in OPENID_SORT_ORDER: 104 | try: 105 | ns_output.append(" %s = %s" % (key, values.pop(key))) 106 | except KeyError: 107 | pass 108 | 109 | values = values.items() 110 | values.sort() 111 | 112 | for k, v in values: 113 | ns_output.append(" %s = %s" % (k, v)) 114 | 115 | output.append('\n'.join(ns_output)) 116 | 117 | return '\n\n'.join(output) 118 | 119 | 120 | def unlistify(d): 121 | return dict((i[0], i[1][0]) for i in d.items()) 122 | 123 | 124 | def queriesFromLogs(s): 125 | qre = re.compile(r'GET (/.*)?\?(.+) HTTP') 126 | 127 | return [(match.group(1), parse_qs(match.group(2))) 128 | for match in qre.finditer(s)] 129 | 130 | 131 | def queriesFromPostdata(s): 132 | # This looks for query data in a line that starts POSTDATA=. 133 | # Tamperdata outputs such lines. If there's a 'Host=' in that block, 134 | # use that too, but don't require it. 135 | qre = re.compile(r'(?:^Host=(?P.+?)$.*?)?^POSTDATA=(?P.*)$', 136 | re.DOTALL | re.MULTILINE) 137 | return [(match.group('host') or 'POSTDATA', 138 | parse_qs(match.group('query'))) for match in qre.finditer(s)] 139 | 140 | 141 | def find_urls(s): 142 | # Regular expression borrowed from urlscan 143 | # by Daniel Burrows , GPL. 144 | urlinternalpattern = r'[{}a-zA-Z/\-_0-9%?&.=:;+,#~]' 145 | urltrailingpattern = r'[{}a-zA-Z/\-_0-9%&=+#]' 146 | httpurlpattern = r'(?:https?://' + urlinternalpattern + r'*' + urltrailingpattern + r')' 147 | # Used to guess that blah.blah.blah.TLD is a URL. 148 | tlds = ['biz', 'com', 'edu', 'info', 'org'] 149 | guessedurlpattern = r'(?:[a-zA-Z0-9_\-%]+(?:\.[a-zA-Z0-9_\-%]+)*\.(?:' + '|'.join( 150 | tlds) + '))' 151 | urlre = re.compile( 152 | 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 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # Test dependencies 2 | coveralls 3 | coverage 4 | 5 | # Package dependencies -- keep these reasonably up to date, if not bleeding- 6 | # edge -- also try to maintain compatibility with all the Python versions we 7 | # support 8 | Django 9 | httplib2 10 | mysqlclient 11 | psycopg2 ; implementation_name == "cpython" 12 | psycopg2cffi ; implementation_name == "pypy" 13 | pycrypto ; python_version < "3.7" and implementation_name == "cpython" 14 | pycurl ; python_version < "3.6" and implementation_name == "cpython" 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/necaris/python3-openid/fbc7f6a2dc5159b890119a044215a6aee4cefc8d/examples/__init__.py -------------------------------------------------------------------------------- /examples/discover: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from openid.consumer.discover import discover, DiscoveryFailure 3 | from openid.fetchers import HTTPFetchingError 4 | 5 | names = [ 6 | ["server_url", "Server URL "], 7 | ["local_id", "Local ID "], 8 | ["canonicalID", "Canonical ID"], 9 | ] 10 | 11 | 12 | def show_services(user_input, normalized, services): 13 | print " Claimed identifier:", normalized 14 | if services: 15 | print " Discovered OpenID services:" 16 | for n, service in enumerate(services): 17 | print " %s." % (n, ) 18 | for attr, name in names: 19 | val = getattr(service, attr, None) 20 | if val is not None: 21 | print " %s: %s" % (name, val) 22 | 23 | print " Type URIs:" 24 | for type_uri in service.type_uris: 25 | print " *", type_uri 26 | 27 | print 28 | 29 | else: 30 | print " No OpenID services found" 31 | print 32 | 33 | 34 | if __name__ == "__main__": 35 | import sys 36 | 37 | for user_input in sys.argv[1:]: 38 | print "=" * 50 39 | print "Running discovery on", user_input 40 | try: 41 | normalized, services = discover(user_input) 42 | except DiscoveryFailure, why: 43 | print "Discovery failed:", why 44 | print 45 | except HTTPFetchingError, why: 46 | print "HTTP request failed:", why 47 | print 48 | else: 49 | show_services(user_input, normalized, services) 50 | -------------------------------------------------------------------------------- /examples/djopenid/README: -------------------------------------------------------------------------------- 1 | DJANGO EXAMPLE PACKAGE 2 | ====================== 3 | 4 | This package implements an example consumer and server for the Django 5 | Python web framework. You can get Django (and learn more about it) at 6 | 7 | http://www.djangoproject.com/ 8 | 9 | SETUP 10 | ===== 11 | 12 | 1. Install the OpenID library 13 | 14 | 2. Install Django >= 1.5 (with Python 3 support) 15 | 16 | 3. Modify djopenid/settings.py appropriately; you may wish to change 17 | the database type or path, although the default settings should be 18 | sufficient for most systems. 19 | 20 | 4. In examples/djopenid/ run: 21 | 22 | python manage.py syncdb 23 | 24 | 5. To run the example consumer or server, run 25 | 26 | python manage.py runserver PORT 27 | 28 | where PORT is the port number on which to listen. 29 | 30 | Note that if you want to try both the consumer and server at the 31 | same time, run the command twice with two different values for 32 | PORT. 33 | 34 | 6. Point your web browser at the server at 35 | 36 | http://localhost:PORT/ 37 | 38 | to begin. 39 | 40 | ABOUT THE CODE 41 | ============== 42 | 43 | The example server and consumer code provided in this package are 44 | intended to be instructional in the use of this OpenID library. While 45 | it is not recommended to use the example code in production, the code 46 | should be sufficient to explain the general use of the library. 47 | 48 | If you aren't familiar with the Django web framework, you can quickly 49 | start looking at the important code by looking in the 'views' modules: 50 | 51 | djopenid.consumer.views 52 | djopenid.server.views 53 | 54 | Each view is a python callable that responds to an HTTP request. 55 | -------------------------------------------------------------------------------- /examples/djopenid/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/necaris/python3-openid/fbc7f6a2dc5159b890119a044215a6aee4cefc8d/examples/djopenid/__init__.py -------------------------------------------------------------------------------- /examples/djopenid/consumer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/necaris/python3-openid/fbc7f6a2dc5159b890119a044215a6aee4cefc8d/examples/djopenid/consumer/__init__.py -------------------------------------------------------------------------------- /examples/djopenid/consumer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/djopenid/consumer/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns( 4 | 'djopenid.consumer.views', 5 | (r'^$', 'startOpenID'), 6 | (r'^finish/$', 'finishOpenID'), 7 | (r'^xrds/$', 'rpXRDS'), ) 8 | -------------------------------------------------------------------------------- /examples/djopenid/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write( 8 | "Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" 9 | % __file__) 10 | sys.exit(1) 11 | 12 | if __name__ == "__main__": 13 | execute_manager(settings) 14 | -------------------------------------------------------------------------------- /examples/djopenid/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/necaris/python3-openid/fbc7f6a2dc5159b890119a044215a6aee4cefc8d/examples/djopenid/server/__init__.py -------------------------------------------------------------------------------- /examples/djopenid/server/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/djopenid/server/tests.py: -------------------------------------------------------------------------------- 1 | from django.test.testcases import TestCase 2 | from djopenid.server import views 3 | from djopenid import util 4 | 5 | from django.http import HttpRequest 6 | from django.contrib.sessions.backends.cache import SessionStore 7 | 8 | from openid.server.server import CheckIDRequest 9 | from openid.message import Message 10 | from openid.yadis.constants import YADIS_CONTENT_TYPE 11 | from openid.yadis.services import applyFilter 12 | 13 | 14 | def dummyRequest(): 15 | request = HttpRequest() 16 | request.session = SessionStore() 17 | request.META['HTTP_HOST'] = 'example.invalid' 18 | request.META['SERVER_PROTOCOL'] = 'HTTP' 19 | return request 20 | 21 | 22 | class TestProcessTrustResult(TestCase): 23 | def setUp(self): 24 | self.request = dummyRequest() 25 | 26 | id_url = util.getViewURL(self.request, views.idPage) 27 | 28 | # Set up the OpenID request we're responding to. 29 | op_endpoint = 'http://127.0.0.1:8080/endpoint' 30 | message = Message.fromPostArgs({ 31 | 'openid.mode': 32 | 'checkid_setup', 33 | 'openid.identity': 34 | id_url, 35 | 'openid.return_to': 36 | 'http://127.0.0.1/%s' % (self.id(), ), 37 | 'openid.sreg.required': 38 | 'postcode', 39 | }) 40 | self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint) 41 | 42 | views.setRequest(self.request, self.openid_request) 43 | 44 | def test_allow(self): 45 | self.request.POST['allow'] = 'Yes' 46 | 47 | response = views.processTrustResult(self.request) 48 | 49 | self.assertEqual(response.status_code, 302) 50 | finalURL = response['location'] 51 | self.assertIn('openid.mode=id_res', finalURL) 52 | self.assertIn('openid.identity=', finalURL) 53 | self.assertIn('openid.sreg.postcode=12345', finalURL) 54 | 55 | def test_cancel(self): 56 | self.request.POST['cancel'] = 'Yes' 57 | 58 | response = views.processTrustResult(self.request) 59 | 60 | self.assertEqual(response.status_code, 302) 61 | finalURL = response['location'] 62 | self.assertIn('openid.mode=cancel', finalURL) 63 | self.assertNotIn('openid.identity=', finalURL) 64 | self.assertNotIn('openid.sreg.postcode=12345', finalURL) 65 | 66 | 67 | class TestShowDecidePage(TestCase): 68 | def test_unreachableRealm(self): 69 | self.request = dummyRequest() 70 | 71 | id_url = util.getViewURL(self.request, views.idPage) 72 | 73 | # Set up the OpenID request we're responding to. 74 | op_endpoint = 'http://127.0.0.1:8080/endpoint' 75 | message = Message.fromPostArgs({ 76 | 'openid.mode': 77 | 'checkid_setup', 78 | 'openid.identity': 79 | id_url, 80 | 'openid.return_to': 81 | 'http://unreachable.invalid/%s' % (self.id(), ), 82 | 'openid.sreg.required': 83 | 'postcode', 84 | }) 85 | self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint) 86 | 87 | views.setRequest(self.request, self.openid_request) 88 | 89 | response = views.showDecidePage(self.request, self.openid_request) 90 | self.assertContains(response, 'trust_root_valid is Unreachable') 91 | 92 | 93 | class TestGenericXRDS(TestCase): 94 | def test_genericRender(self): 95 | """ 96 | Render XRDS document with a single type URI and a single endpoint URL 97 | Parse it to see that it matches. 98 | """ 99 | request = dummyRequest() 100 | 101 | type_uris = ['A_TYPE'] 102 | endpoint_url = 'A_URL' 103 | response = util.renderXRDS(request, type_uris, [endpoint_url]) 104 | 105 | requested_url = 'http://requested.invalid/' 106 | (endpoint, ) = applyFilter(requested_url, response.content) 107 | 108 | self.assertEqual(YADIS_CONTENT_TYPE, response['Content-Type']) 109 | self.assertEqual(type_uris, endpoint.type_uris) 110 | self.assertEqual(endpoint_url, endpoint.uri) 111 | -------------------------------------------------------------------------------- /examples/djopenid/server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns( 4 | 'djopenid.server.views', 5 | (r'^$', 'server'), 6 | (r'^xrds/$', 'idpXrds'), 7 | (r'^processTrustResult/$', 'processTrustResult'), 8 | (r'^user/$', 'idPage'), 9 | (r'^endpoint/$', 'endpoint'), 10 | (r'^trust/$', 'trustPage'), ) 11 | -------------------------------------------------------------------------------- /examples/djopenid/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djopenid example project 3 | """ 4 | import os 5 | import sys 6 | import warnings 7 | 8 | try: 9 | import openid 10 | except ImportError as e: 11 | warnings.warn( 12 | "Could not import OpenID. Please consult the djopenid README.") 13 | sys.exit(1) 14 | 15 | DEBUG = True 16 | TEMPLATE_DEBUG = DEBUG 17 | 18 | ADMINS = ( # ('Your Name', 'your_email@domain.com'), 19 | ) 20 | 21 | MANAGERS = ADMINS 22 | 23 | # NOTE that this is a sample configuration and probably not suitable for 24 | # production use in any way, shape or form. 25 | DATABASES = { 26 | 'default': { 27 | 'ENGINE': 'django.db.backends.sqlite3', 28 | 'NAME': './sqlite.db' 29 | } 30 | } 31 | 32 | # Local time zone for this installation. All choices can be found here: 33 | # http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE 34 | TIME_ZONE = 'Europe/London' 35 | 36 | # Language code for this installation. All choices can be found here: 37 | # http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes 38 | # http://blogs.law.harvard.edu/tech/stories/storyReader$15 39 | LANGUAGE_CODE = 'en-us.UTF-8' 40 | USE_I18N = False 41 | 42 | SITE_ID = 1 43 | 44 | # Absolute path to the directory that holds media. 45 | # Example: "/home/media/media.lawrence.com/" 46 | MEDIA_ROOT = '' 47 | 48 | # URL that handles the media served from MEDIA_ROOT. 49 | # Example: "http://media.lawrence.com" 50 | MEDIA_URL = '' 51 | 52 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 53 | # trailing slash. 54 | # Examples: "http://foo.com/media/", "/media/". 55 | ADMIN_MEDIA_PREFIX = '/media/' 56 | 57 | # Make this unique, and don't share it with anybody. 58 | SECRET_KEY = 'u^bw6lmsa6fah0$^lz-ct$)y7x7#ag92-z+y45-8!(jk0lkavy' 59 | 60 | # List of callables that know how to import templates from various sources. 61 | TEMPLATE_LOADERS = ('django.template.loaders.filesystem.Loader', 62 | 'django.template.loaders.app_directories.Loader', ) 63 | 64 | MIDDLEWARE_CLASSES = ( 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.contrib.sessions.middleware.SessionMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 'django.middleware.doc.XViewMiddleware', ) 69 | 70 | ROOT_URLCONF = 'djopenid.urls' 71 | 72 | TEMPLATE_CONTEXT_PROCESSORS = () 73 | 74 | TEMPLATE_DIRS = ( 75 | os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates')), ) 76 | 77 | INSTALLED_APPS = ( 78 | 'django.contrib.contenttypes', 79 | 'django.contrib.sessions', 80 | # These are the example OpenID consumer and server 81 | 'djopenid.consumer', 82 | 'djopenid.server', ) 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/djopenid/templates/consumer/request_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ html|safe }} 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | {{ user_url }}
45 |     
46 |

47 | 48 |
49 | {% endblock %} 50 | 51 | 52 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/djopenid/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns( 4 | '', 5 | ('^$', 'djopenid.views.index'), 6 | ('^consumer/', include('djopenid.consumer.urls')), 7 | ('^server/', include('djopenid.server.urls')), ) 8 | -------------------------------------------------------------------------------- /examples/djopenid/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility code for the Django example consumer and server. 3 | """ 4 | from urllib.parse import urljoin 5 | 6 | from django.db import connection 7 | from django.template.context import RequestContext 8 | from django.template import loader 9 | from django import http 10 | from django.core.exceptions import ImproperlyConfigured 11 | from django.core.urlresolvers import reverse as reverseURL 12 | from django.shortcuts import render_to_response 13 | 14 | from django.conf import settings 15 | 16 | try: 17 | import psycopg2 18 | except ImportError: 19 | from psycopg2cffi import compat 20 | compat.register() 21 | 22 | from openid.store.filestore import FileOpenIDStore 23 | from openid.store import sqlstore 24 | from openid.yadis.constants import YADIS_CONTENT_TYPE 25 | 26 | 27 | def getOpenIDStore(filestore_path, table_prefix): 28 | """ 29 | Returns an OpenID association store object based on the database 30 | engine chosen for this Django application. 31 | 32 | * If no database engine is chosen, a filesystem-based store will 33 | be used whose path is filestore_path. 34 | 35 | * If a database engine is chosen, a store object for that database 36 | type will be returned. 37 | 38 | * If the chosen engine is not supported by the OpenID library, 39 | raise ImproperlyConfigured. 40 | 41 | * If a database store is used, this will create the tables 42 | necessary to use it. The table names will be prefixed with 43 | table_prefix. DO NOT use the same table prefix for both an 44 | OpenID consumer and an OpenID server in the same database. 45 | 46 | The result of this function should be passed to the Consumer 47 | constructor as the store parameter. 48 | """ 49 | 50 | db_engine = settings.DATABASES['default']['ENGINE'] 51 | if not db_engine: 52 | return FileOpenIDStore(filestore_path) 53 | 54 | # Possible side-effect: create a database connection if one isn't 55 | # already open. 56 | connection.cursor() 57 | 58 | # Create table names to specify for SQL-backed stores. 59 | tablenames = { 60 | 'associations_table': table_prefix + 'openid_associations', 61 | 'nonces_table': table_prefix + 'openid_nonces', 62 | } 63 | 64 | types = { 65 | 'django.db.backends.postgresql_psycopg2': sqlstore.PostgreSQLStore, 66 | 'django.db.backends.mysql': sqlstore.MySQLStore, 67 | 'django.db.backends.sqlite3': sqlstore.SQLiteStore, 68 | } 69 | 70 | if db_engine not in types: 71 | raise ImproperlyConfigured( 72 | "Database engine %s not supported by OpenID library" % db_engine) 73 | 74 | s = types[db_engine](connection.connection, **tablenames) 75 | 76 | try: 77 | s.createTables() 78 | except (SystemExit, KeyboardInterrupt, MemoryError): 79 | raise 80 | except: 81 | # XXX This is not the Right Way to do this, but because the 82 | # underlying database implementation might differ in behavior 83 | # at this point, we can't reliably catch the right 84 | # exception(s) here. Ideally, the SQL store in the OpenID 85 | # library would catch exceptions that it expects and fail 86 | # silently, but that could be bad, too. More ideally, the SQL 87 | # store would not attempt to create tables it knows already 88 | # exists. 89 | pass 90 | 91 | return s 92 | 93 | 94 | def getViewURL(req, view_name_or_obj, args=None, kwargs=None): 95 | relative_url = reverseURL(view_name_or_obj, args=args, kwargs=kwargs) 96 | full_path = req.META.get('SCRIPT_NAME', '') + relative_url 97 | return urljoin(getBaseURL(req), full_path) 98 | 99 | 100 | def getBaseURL(req): 101 | """ 102 | Given a Django web request object, returns the OpenID 'trust root' 103 | for that request; namely, the absolute URL to the site root which 104 | is serving the Django request. The trust root will include the 105 | proper scheme and authority. It will lack a port if the port is 106 | standard (80, 443). 107 | """ 108 | name = req.META['HTTP_HOST'] 109 | try: 110 | name = name[:name.index(':')] 111 | except: 112 | pass 113 | 114 | try: 115 | port = int(req.META['SERVER_PORT']) 116 | except: 117 | port = 80 118 | 119 | proto = req.META['SERVER_PROTOCOL'] 120 | 121 | if 'HTTPS' in proto: 122 | proto = 'https' 123 | else: 124 | proto = 'http' 125 | 126 | if port in [80, 443] or not port: 127 | port = '' 128 | else: 129 | port = ':%s' % (port, ) 130 | 131 | url = "%s://%s%s/" % (proto, name, port) 132 | return url 133 | 134 | 135 | def normalDict(request_data): 136 | """ 137 | Converts a django request MutliValueDict (e.g., request.GET, 138 | request.POST) into a standard python dict whose values are the 139 | first value from each of the MultiValueDict's value lists. This 140 | avoids the OpenID library's refusal to deal with dicts whose 141 | values are lists, because in OpenID, each key in the query arg set 142 | can have at most one value. 143 | """ 144 | return dict((k, v) for k, v in request_data.items()) 145 | 146 | 147 | def renderXRDS(request, type_uris, endpoint_urls): 148 | """Render an XRDS page with the specified type URIs and endpoint 149 | URLs in one service block, and return a response with the 150 | appropriate content-type. 151 | """ 152 | response = render_to_response( 153 | 'xrds.xml', {'type_uris': type_uris, 154 | 'endpoint_urls': endpoint_urls}, 155 | context_instance=RequestContext(request)) 156 | response['Content-Type'] = YADIS_CONTENT_TYPE 157 | return response 158 | -------------------------------------------------------------------------------- /examples/djopenid/views.py: -------------------------------------------------------------------------------- 1 | from djopenid import util 2 | from django.views.generic.base import TemplateView 3 | 4 | 5 | def index(request): 6 | consumer_url = util.getViewURL(request, 7 | 'djopenid.consumer.views.startOpenID') 8 | server_url = util.getViewURL(request, 'djopenid.server.views.server') 9 | 10 | return TemplateView(request, 'index.html', { 11 | 'consumer_url': consumer_url, 12 | 'server_url': server_url 13 | }) 14 | -------------------------------------------------------------------------------- /openid/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | """ 3 | This package is an implementation of the OpenID specification in 4 | Python. It contains code for both server and consumer 5 | implementations. For information on implementing an OpenID consumer, 6 | see the C{L{openid.consumer.consumer}} module. For information on 7 | implementing an OpenID server, see the C{L{openid.server.server}} 8 | module. 9 | 10 | @contact: U{http://github.com/necaris/python3-openid/} 11 | 12 | @copyright: (C) 2005-2008 JanRain, Inc., 2012-2017 Rami Chowdhury 13 | 14 | @license: Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | U{http://www.apache.org/licenses/LICENSE-2.0} 18 | 19 | Unless required by applicable law or agreed to in writing, software 20 | distributed under the License is distributed on an "AS IS" BASIS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions 23 | and limitations under the License. 24 | """ 25 | 26 | version_info = (3, 2, 0) 27 | 28 | 29 | 30 | 31 | __version__ = ".".join(str(x) for x in version_info) 32 | 33 | __all__ = [ 34 | 'association', 35 | 'consumer', 36 | 'cryptutil', 37 | 'dh', 38 | 'extension', 39 | 'extensions', 40 | 'fetchers', 41 | 'kvform', 42 | 'message', 43 | 'oidutil', 44 | 'server', 45 | 'sreg', 46 | 'store', 47 | 'urinorm', 48 | 'yadis', 49 | ] 50 | -------------------------------------------------------------------------------- /openid/codecutil.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | 3 | try: 4 | chr(0x10000) 5 | except ValueError: 6 | # narrow python build 7 | UCSCHAR = [ 8 | (0xA0, 0xD7FF), 9 | (0xF900, 0xFDCF), 10 | (0xFDF0, 0xFFEF), 11 | ] 12 | 13 | IPRIVATE = [ 14 | (0xE000, 0xF8FF), 15 | ] 16 | else: 17 | UCSCHAR = [ 18 | (0xA0, 0xD7FF), 19 | (0xF900, 0xFDCF), 20 | (0xFDF0, 0xFFEF), 21 | (0x10000, 0x1FFFD), 22 | (0x20000, 0x2FFFD), 23 | (0x30000, 0x3FFFD), 24 | (0x40000, 0x4FFFD), 25 | (0x50000, 0x5FFFD), 26 | (0x60000, 0x6FFFD), 27 | (0x70000, 0x7FFFD), 28 | (0x80000, 0x8FFFD), 29 | (0x90000, 0x9FFFD), 30 | (0xA0000, 0xAFFFD), 31 | (0xB0000, 0xBFFFD), 32 | (0xC0000, 0xCFFFD), 33 | (0xD0000, 0xDFFFD), 34 | (0xE1000, 0xEFFFD), 35 | ] 36 | 37 | IPRIVATE = [ 38 | (0xE000, 0xF8FF), 39 | (0xF0000, 0xFFFFD), 40 | (0x100000, 0x10FFFD), 41 | ] 42 | 43 | _ESCAPE_RANGES = UCSCHAR + IPRIVATE 44 | 45 | 46 | def _in_escape_range(octet): 47 | for start, end in _ESCAPE_RANGES: 48 | if start <= octet <= end: 49 | return True 50 | return False 51 | 52 | 53 | def _starts_surrogate_pair(character): 54 | char_value = ord(character) 55 | return 0xD800 <= char_value <= 0xDBFF 56 | 57 | 58 | def _ends_surrogate_pair(character): 59 | char_value = ord(character) 60 | return 0xDC00 <= char_value <= 0xDFFF 61 | 62 | 63 | def _pct_encoded_replacements(chunk): 64 | replacements = [] 65 | chunk_iter = iter(chunk) 66 | for character in chunk_iter: 67 | codepoint = ord(character) 68 | if _in_escape_range(codepoint): 69 | for char in chr(codepoint).encode("utf-8"): 70 | replacements.append("%%%X" % char) 71 | elif _starts_surrogate_pair(character): 72 | next_character = next(chunk_iter) 73 | for char in (character + next_character).encode("utf-8"): 74 | replacements.append("%%%X" % char) 75 | else: 76 | replacements.append(chr(codepoint)) 77 | return replacements 78 | 79 | 80 | def _pct_escape_handler(err): 81 | ''' 82 | Encoding error handler that does percent-escaping of Unicode, to be used 83 | with codecs.register_error 84 | TODO: replace use of this with urllib.parse.quote as appropriate 85 | ''' 86 | chunk = err.object[err.start:err.end] 87 | replacements = _pct_encoded_replacements(chunk) 88 | return ("".join(replacements), err.end) 89 | 90 | 91 | codecs.register_error("oid_percent_escape", _pct_escape_handler) 92 | -------------------------------------------------------------------------------- /openid/consumer/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains the portions of the library used only when 3 | implementing an OpenID consumer. 4 | """ 5 | 6 | __all__ = ['consumer', 'discover'] 7 | -------------------------------------------------------------------------------- /openid/cryptutil.py: -------------------------------------------------------------------------------- 1 | """Module containing a cryptographic-quality source of randomness and 2 | other cryptographically useful functionality 3 | 4 | Python 2.4 needs no external support for this module, nor does Python 5 | 2.3 on a system with /dev/urandom. 6 | 7 | Other configurations will need a quality source of random bytes and 8 | access to a function that will convert binary strings to long 9 | integers. This module will work with the Python Cryptography Toolkit 10 | (pycrypto) if it is present. pycrypto can be found with a search 11 | engine, but is currently found at: 12 | 13 | http://www.amk.ca/python/code/crypto 14 | """ 15 | 16 | __all__ = [ 17 | 'base64ToLong', 18 | 'binaryToLong', 19 | 'hmacSha1', 20 | 'hmacSha256', 21 | 'longToBase64', 22 | 'longToBinary', 23 | 'randomString', 24 | 'randrange', 25 | 'sha1', 26 | 'sha256', 27 | ] 28 | 29 | import hmac 30 | import os 31 | import random 32 | 33 | from openid.oidutil import toBase64, fromBase64 34 | 35 | import hashlib 36 | 37 | 38 | class HashContainer(object): 39 | def __init__(self, hash_constructor): 40 | self.new = hash_constructor 41 | self.digest_size = hash_constructor().digest_size 42 | 43 | 44 | sha1_module = HashContainer(hashlib.sha1) 45 | sha256_module = HashContainer(hashlib.sha256) 46 | 47 | 48 | def hmacSha1(key, text): 49 | if isinstance(key, str): 50 | key = bytes(key, encoding="utf-8") 51 | if isinstance(text, str): 52 | text = bytes(text, encoding="utf-8") 53 | return hmac.new(key, text, sha1_module).digest() 54 | 55 | 56 | def sha1(s): 57 | if isinstance(s, str): 58 | s = bytes(s, encoding="utf-8") 59 | return sha1_module.new(s).digest() 60 | 61 | 62 | def hmacSha256(key, text): 63 | if isinstance(key, str): 64 | key = bytes(key, encoding="utf-8") 65 | if isinstance(text, str): 66 | text = bytes(text, encoding="utf-8") 67 | return hmac.new(key, text, sha256_module).digest() 68 | 69 | 70 | def sha256(s): 71 | if isinstance(s, str): 72 | s = bytes(s, encoding="utf-8") 73 | return sha256_module.new(s).digest() 74 | 75 | 76 | SHA256_AVAILABLE = True 77 | 78 | try: 79 | from Crypto.Util.number import long_to_bytes, bytes_to_long 80 | except ImportError: 81 | # In the case where we don't have pycrypto installed, define substitute 82 | # functionality. 83 | 84 | import pickle 85 | 86 | def longToBinary(l): 87 | if l == 0: 88 | return b'\x00' 89 | b = bytearray(pickle.encode_long(l)) 90 | b.reverse() 91 | return bytes(b) 92 | 93 | def binaryToLong(s): 94 | if isinstance(s, str): 95 | s = s.encode("utf-8") 96 | b = bytearray(s) 97 | b.reverse() 98 | return pickle.decode_long(bytes(b)) 99 | else: 100 | # We have pycrypto, so wrap its functions instead. 101 | 102 | def longToBinary(l): 103 | if l < 0: 104 | raise ValueError('This function only supports positive integers') 105 | 106 | bytestring = long_to_bytes(l) 107 | if bytestring[0] > 127: 108 | return b'\x00' + bytestring 109 | else: 110 | return bytestring 111 | 112 | def binaryToLong(bytestring): 113 | if not bytestring: 114 | raise ValueError('Empty string passed to strToLong') 115 | 116 | if bytestring[0] > 127: 117 | raise ValueError('This function only supports positive integers') 118 | 119 | return bytes_to_long(bytestring) 120 | 121 | 122 | # A cryptographically safe source of random bytes 123 | getBytes = os.urandom 124 | 125 | # A randrange function that works for longs 126 | randrange = random.randrange 127 | 128 | 129 | def longToBase64(l): 130 | return toBase64(longToBinary(l)) 131 | 132 | 133 | def base64ToLong(s): 134 | return binaryToLong(fromBase64(s)) 135 | 136 | 137 | def randomString(length, chrs=None): 138 | """Produce a string of length random bytes, chosen from chrs.""" 139 | if chrs is None: 140 | return getBytes(length) 141 | else: 142 | n = len(chrs) 143 | return ''.join([chrs[randrange(n)] for _ in range(length)]) 144 | 145 | 146 | def const_eq(s1, s2): 147 | if len(s1) != len(s2): 148 | return False 149 | 150 | result = True 151 | for i in range(len(s1)): 152 | result = result and (s1[i] == s2[i]) 153 | 154 | return result 155 | -------------------------------------------------------------------------------- /openid/dh.py: -------------------------------------------------------------------------------- 1 | from openid import cryptutil 2 | 3 | 4 | def strxor(x, y): 5 | if len(x) != len(y): 6 | raise ValueError('Inputs to strxor must have the same length') 7 | 8 | if isinstance(x, str): 9 | x = x.encode("utf-8") 10 | if isinstance(y, str): 11 | y = y.encode("utf-8") 12 | 13 | return bytes([a ^ b for a, b in zip(x, y)]) 14 | 15 | 16 | class DiffieHellman(object): 17 | DEFAULT_MOD = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443 18 | 19 | DEFAULT_GEN = 2 20 | 21 | def fromDefaults(cls): 22 | return cls(cls.DEFAULT_MOD, cls.DEFAULT_GEN) 23 | 24 | fromDefaults = classmethod(fromDefaults) 25 | 26 | def __init__(self, modulus, generator): 27 | self.modulus = int(modulus) 28 | self.generator = int(generator) 29 | 30 | self._setPrivate(cryptutil.randrange(1, modulus - 1)) 31 | 32 | def _setPrivate(self, private): 33 | """This is here to make testing easier""" 34 | self.private = private 35 | self.public = pow(self.generator, self.private, self.modulus) 36 | 37 | def usingDefaultValues(self): 38 | return (self.modulus == self.DEFAULT_MOD and 39 | self.generator == self.DEFAULT_GEN) 40 | 41 | def getSharedSecret(self, composite): 42 | return pow(composite, self.private, self.modulus) 43 | 44 | def xorSecret(self, composite, secret, hash_func): 45 | dh_shared = self.getSharedSecret(composite) 46 | hashed_dh_shared = hash_func(cryptutil.longToBinary(dh_shared)) 47 | return strxor(secret, hashed_dh_shared) 48 | -------------------------------------------------------------------------------- /openid/extension.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from openid import message as message_module 4 | 5 | 6 | class Extension(object): 7 | """An interface for OpenID extensions. 8 | 9 | @ivar ns_uri: The namespace to which to add the arguments for this 10 | extension 11 | """ 12 | ns_uri = None 13 | ns_alias = None 14 | 15 | def getExtensionArgs(self): 16 | """Get the string arguments that should be added to an OpenID 17 | message for this extension. 18 | 19 | @returns: A dictionary of completely non-namespaced arguments 20 | to be added. For example, if the extension's alias is 21 | 'uncle', and this method returns {'meat':'Hot Rats'}, the 22 | final message will contain {'openid.uncle.meat':'Hot Rats'} 23 | """ 24 | raise NotImplementedError() 25 | 26 | def toMessage(self, message=None): 27 | """Add the arguments from this extension to the provided 28 | message, or create a new message containing only those 29 | arguments. 30 | 31 | @returns: The message with the extension arguments added 32 | """ 33 | if message is None: 34 | warnings.warn( 35 | 'Passing None to Extension.toMessage is deprecated. ' 36 | 'Creating a message assuming you want OpenID 2.', 37 | DeprecationWarning, 38 | stacklevel=2) 39 | message = message_module.Message(message_module.OPENID2_NS) 40 | 41 | implicit = message.isOpenID1() 42 | 43 | try: 44 | message.namespaces.addAlias( 45 | self.ns_uri, self.ns_alias, 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/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | """OpenID Extension modules.""" 2 | 3 | __all__ = ['ax', 'pape', 'sreg'] 4 | 5 | from openid.extensions.draft import pape5 as pape 6 | -------------------------------------------------------------------------------- /openid/extensions/draft/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/necaris/python3-openid/fbc7f6a2dc5159b890119a044215a6aee4cefc8d/openid/extensions/draft/__init__.py -------------------------------------------------------------------------------- /openid/kvform.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | __all__ = ['seqToKV', 'kvToSeq', 'dictToKV', 'kvToDict'] 6 | 7 | 8 | class KVFormError(ValueError): 9 | pass 10 | 11 | 12 | def seqToKV(seq, strict=False): 13 | """Represent a sequence of pairs of strings as newline-terminated 14 | key:value pairs. The pairs are generated in the order given. 15 | 16 | @param seq: The pairs 17 | @type seq: [(str, (unicode|str))] 18 | 19 | @return: A string representation of the sequence 20 | @rtype: bytes 21 | """ 22 | 23 | def err(msg): 24 | formatted = 'seqToKV warning: %s: %r' % (msg, seq) 25 | if strict: 26 | raise KVFormError(formatted) 27 | else: 28 | logger.warning(formatted) 29 | 30 | lines = [] 31 | for k, v in seq: 32 | if isinstance(k, bytes): 33 | k = k.decode('utf-8') 34 | elif not isinstance(k, str): 35 | err('Converting key to string: %r' % k) 36 | k = str(k) 37 | 38 | if '\n' in k: 39 | raise KVFormError( 40 | 'Invalid input for seqToKV: key contains newline: %r' % (k, )) 41 | 42 | if ':' in k: 43 | raise KVFormError( 44 | 'Invalid input for seqToKV: key contains colon: %r' % (k, )) 45 | 46 | if k.strip() != k: 47 | err('Key has whitespace at beginning or end: %r' % (k, )) 48 | 49 | if isinstance(v, bytes): 50 | v = v.decode('utf-8') 51 | elif not isinstance(v, str): 52 | err('Converting value to string: %r' % (v, )) 53 | v = str(v) 54 | 55 | if '\n' in v: 56 | raise KVFormError( 57 | 'Invalid input for seqToKV: value contains newline: %r' % 58 | (v, )) 59 | 60 | if v.strip() != v: 61 | err('Value has whitespace at beginning or end: %r' % (v, )) 62 | 63 | lines.append(k + ':' + v + '\n') 64 | 65 | return ''.join(lines).encode('utf-8') 66 | 67 | 68 | def kvToSeq(data, strict=False): 69 | """ 70 | 71 | After one parse, seqToKV and kvToSeq are inverses, with no warnings:: 72 | 73 | seq = kvToSeq(s) 74 | seqToKV(kvToSeq(seq)) == seq 75 | 76 | @return str 77 | """ 78 | 79 | def err(msg): 80 | formatted = 'kvToSeq warning: %s: %r' % (msg, data) 81 | if strict: 82 | raise KVFormError(formatted) 83 | else: 84 | logger.warning(formatted) 85 | 86 | if isinstance(data, bytes): 87 | data = data.decode("utf-8") 88 | 89 | lines = data.split('\n') 90 | if lines[-1]: 91 | err('Does not end in a newline') 92 | else: 93 | del lines[-1] 94 | 95 | pairs = [] 96 | line_num = 0 97 | for line in lines: 98 | line_num += 1 99 | 100 | # Ignore blank lines 101 | if not line.strip(): 102 | continue 103 | 104 | pair = line.split(':', 1) 105 | if len(pair) == 2: 106 | k, v = pair 107 | k_s = k.strip() 108 | if k_s != k: 109 | fmt = ('In line %d, ignoring leading or trailing ' 110 | 'whitespace in key %r') 111 | err(fmt % (line_num, k)) 112 | 113 | if not k_s: 114 | err('In line %d, got empty key' % (line_num, )) 115 | 116 | v_s = v.strip() 117 | if v_s != v: 118 | fmt = ('In line %d, ignoring leading or trailing ' 119 | 'whitespace in value %r') 120 | err(fmt % (line_num, v)) 121 | 122 | pairs.append((k_s, v_s)) 123 | else: 124 | err('Line %d does not contain a colon' % line_num) 125 | 126 | return pairs 127 | 128 | 129 | def dictToKV(d): 130 | return seqToKV(sorted(d.items())) 131 | 132 | 133 | def kvToDict(s): 134 | return dict(kvToSeq(s)) 135 | -------------------------------------------------------------------------------- /openid/server/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package contains the portions of the library used only when 3 | implementing an OpenID server. See L{openid.server.server}. 4 | """ 5 | 6 | __all__ = ['server', 'trustroot'] 7 | -------------------------------------------------------------------------------- /openid/sreg.py: -------------------------------------------------------------------------------- 1 | """moved to L{openid.extensions.sreg}""" 2 | 3 | import warnings 4 | warnings.warn("openid.sreg has moved to openid.extensions.sreg", 5 | DeprecationWarning) 6 | 7 | from openid.extensions.sreg import * 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 | 8 | __all__ = ['interface', 'filestore', 'sqlstore', 'memstore', 'nonce'] 9 | -------------------------------------------------------------------------------- /openid/store/memstore.py: -------------------------------------------------------------------------------- 1 | """A simple store using only in-process memory.""" 2 | 3 | from openid.store import nonce 4 | 5 | import copy 6 | import time 7 | 8 | 9 | class ServerAssocs(object): 10 | def __init__(self): 11 | self.assocs = {} 12 | 13 | def set(self, assoc): 14 | self.assocs[assoc.handle] = assoc 15 | 16 | def get(self, handle): 17 | return self.assocs.get(handle) 18 | 19 | def remove(self, handle): 20 | try: 21 | del self.assocs[handle] 22 | except KeyError: 23 | return False 24 | else: 25 | return True 26 | 27 | def best(self): 28 | """Returns association with the oldest issued date. 29 | 30 | or None if there are no associations. 31 | """ 32 | best = None 33 | for assoc in list(self.assocs.values()): 34 | if best is None or best.issued < assoc.issued: 35 | best = assoc 36 | return best 37 | 38 | def cleanup(self): 39 | """Remove expired associations. 40 | 41 | @return: tuple of (removed associations, remaining associations) 42 | """ 43 | remove = [] 44 | for handle, assoc in self.assocs.items(): 45 | if assoc.expiresIn == 0: 46 | remove.append(handle) 47 | for handle in remove: 48 | del self.assocs[handle] 49 | return len(remove), len(self.assocs) 50 | 51 | 52 | class MemoryStore(object): 53 | """In-process memory store. 54 | 55 | Use for single long-running processes. No persistence supplied. 56 | """ 57 | 58 | def __init__(self): 59 | self.server_assocs = {} 60 | self.nonces = {} 61 | 62 | def _getServerAssocs(self, server_url): 63 | try: 64 | return self.server_assocs[server_url] 65 | except KeyError: 66 | assocs = self.server_assocs[server_url] = ServerAssocs() 67 | return assocs 68 | 69 | def storeAssociation(self, server_url, assoc): 70 | assocs = self._getServerAssocs(server_url) 71 | assocs.set(copy.deepcopy(assoc)) 72 | 73 | def getAssociation(self, server_url, handle=None): 74 | assocs = self._getServerAssocs(server_url) 75 | if handle is None: 76 | return assocs.best() 77 | else: 78 | return assocs.get(handle) 79 | 80 | def removeAssociation(self, server_url, handle): 81 | assocs = self._getServerAssocs(server_url) 82 | return assocs.remove(handle) 83 | 84 | def useNonce(self, server_url, timestamp, salt): 85 | if abs(timestamp - time.time()) > nonce.SKEW: 86 | return False 87 | 88 | anonce = (str(server_url), int(timestamp), str(salt)) 89 | if anonce in self.nonces: 90 | return False 91 | else: 92 | self.nonces[anonce] = None 93 | return True 94 | 95 | def cleanupNonces(self): 96 | now = time.time() 97 | expired = [] 98 | for anonce in self.nonces.keys(): 99 | if abs(anonce[1] - now) > nonce.SKEW: 100 | # removing items while iterating over the set could be bad. 101 | expired.append(anonce) 102 | 103 | for anonce in expired: 104 | del self.nonces[anonce] 105 | return len(expired) 106 | 107 | def cleanupAssociations(self): 108 | remove_urls = [] 109 | removed_assocs = 0 110 | for server_url, assocs in self.server_assocs.items(): 111 | removed, remaining = assocs.cleanup() 112 | removed_assocs += removed 113 | if not remaining: 114 | remove_urls.append(server_url) 115 | 116 | # Remove entries from server_assocs that had none remaining. 117 | for server_url in remove_urls: 118 | del self.server_assocs[server_url] 119 | return removed_assocs 120 | 121 | def __eq__(self, other): 122 | return ((self.server_assocs == other.server_assocs) and 123 | (self.nonces == other.nonces)) 124 | 125 | def __ne__(self, other): 126 | return not (self == other) 127 | -------------------------------------------------------------------------------- /openid/store/nonce.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'split', 3 | 'mkNonce', 4 | 'checkTimestamp', 5 | ] 6 | 7 | from openid import cryptutil 8 | from time import strptime, strftime, gmtime, time 9 | from calendar import timegm 10 | import string 11 | 12 | NONCE_CHARS = string.ascii_letters + string.digits 13 | 14 | # Keep nonces for five hours (allow five hours for the combination of 15 | # request time and clock skew). This is probably way more than is 16 | # necessary, but there is not much overhead in storing nonces. 17 | SKEW = 60 * 60 * 5 18 | 19 | time_fmt = '%Y-%m-%dT%H:%M:%SZ' 20 | time_str_len = len('0000-00-00T00:00:00Z') 21 | 22 | 23 | def split(nonce_string): 24 | """Extract a timestamp from the given nonce string 25 | 26 | @param nonce_string: the nonce from which to extract the timestamp 27 | @type nonce_string: str 28 | 29 | @returns: A pair of a Unix timestamp and the salt characters 30 | @returntype: (int, str) 31 | 32 | @raises ValueError: if the nonce does not start with a correctly 33 | formatted time string 34 | """ 35 | timestamp_str = nonce_string[:time_str_len] 36 | try: 37 | timestamp = timegm(strptime(timestamp_str, time_fmt)) 38 | except AssertionError: # Python 2.2 39 | timestamp = -1 40 | if timestamp < 0: 41 | raise ValueError('time out of range') 42 | return timestamp, nonce_string[time_str_len:] 43 | 44 | 45 | def checkTimestamp(nonce_string, allowed_skew=SKEW, now=None): 46 | """Is the timestamp that is part of the specified nonce string 47 | within the allowed clock-skew of the current time? 48 | 49 | @param nonce_string: The nonce that is being checked 50 | @type nonce_string: str 51 | 52 | @param allowed_skew: How many seconds should be allowed for 53 | completing the request, allowing for clock skew. 54 | @type allowed_skew: int 55 | 56 | @param now: The current time, as a Unix timestamp 57 | @type now: int 58 | 59 | @returntype: bool 60 | @returns: Whether the timestamp is correctly formatted and within 61 | the allowed skew of the current time. 62 | """ 63 | try: 64 | stamp, _ = split(nonce_string) 65 | except ValueError: 66 | return False 67 | else: 68 | if now is None: 69 | now = time() 70 | 71 | # Time after which we should not use the nonce 72 | past = now - allowed_skew 73 | 74 | # Time that is too far in the future for us to allow 75 | future = now + allowed_skew 76 | 77 | # the stamp is not too far in the future and is not too far in 78 | # the past 79 | return past <= stamp <= future 80 | 81 | 82 | def mkNonce(when=None): 83 | """Generate a nonce with the current timestamp 84 | 85 | @param when: Unix timestamp representing the issue time of the 86 | nonce. Defaults to the current time. 87 | @type when: int 88 | 89 | @returntype: str 90 | @returns: A string that should be usable as a one-way nonce 91 | 92 | @see: time 93 | """ 94 | salt = cryptutil.randomString(6, NONCE_CHARS) 95 | if when is None: 96 | t = gmtime() 97 | else: 98 | t = gmtime(when) 99 | 100 | time_str = strftime(time_fmt, t) 101 | return time_str + salt 102 | -------------------------------------------------------------------------------- /openid/test/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os.path 3 | import warnings 4 | import unittest 5 | 6 | 7 | def addParentToPath(): 8 | """ 9 | Add the parent directory to sys.path to make it importable. 10 | """ 11 | try: 12 | d = os.path.dirname(__file__) 13 | except NameError: 14 | d = os.path.dirname(sys.argv[0]) 15 | parent = os.path.normpath(os.path.join(d, '..')) 16 | if parent not in sys.path: 17 | print("adding {} to sys.path".format(parent)) 18 | sys.path.insert(0, parent) 19 | 20 | 21 | def specialCaseTests(): 22 | """ 23 | Some modules have an explicit `test` function that collects tests -- 24 | collect these together as a suite. 25 | """ 26 | function_test_modules = [ 27 | 'cryptutil', 28 | 'oidutil', 29 | 'dh', 30 | ] 31 | 32 | suite = unittest.TestSuite() 33 | for module_name in function_test_modules: 34 | module_name = 'openid.test.' + module_name 35 | try: 36 | test_mod = __import__(module_name, {}, {}, [None]) 37 | except ImportError: 38 | print(('Failed to import test %r' % (module_name, ))) 39 | else: 40 | suite.addTest(unittest.FunctionTestCase(test_mod.test)) 41 | 42 | return suite 43 | 44 | 45 | def pyUnitTests(): 46 | """ 47 | Aggregate unit tests from modules, including a few special cases, and 48 | return a suite. 49 | """ 50 | test_module_names = [ 51 | 'server', 52 | 'consumer', 53 | 'message', 54 | 'symbol', 55 | 'etxrd', 56 | 'xri', 57 | 'xrires', 58 | 'association_response', 59 | 'auth_request', 60 | 'negotiation', 61 | 'verifydisco', 62 | 'sreg', 63 | 'ax', 64 | 'pape', 65 | 'pape_draft2', 66 | 'pape_draft5', 67 | 'rpverify', 68 | 'extension', 69 | 'codecutil', 70 | ] 71 | 72 | test_modules = [ 73 | __import__('openid.test.test_{}'.format(name), {}, {}, ['unused']) 74 | for name in test_module_names 75 | ] 76 | 77 | try: 78 | from openid.test import test_examples 79 | except ImportError: 80 | # This is very likely due to twill being unimportable, since it's 81 | # ancient and unmaintained. Until the examples are reimplemented using 82 | # something else, we just need to skip it 83 | warnings.warn("Could not import twill; skipping test_examples.") 84 | else: 85 | test_modules.append(test_examples) 86 | 87 | # Some modules have data-driven tests, and they use custom methods 88 | # to build the test suite -- the module-level pyUnitTests function should 89 | # return an appropriate test suite 90 | custom_module_names = [ 91 | 'kvform', 92 | 'linkparse', 93 | 'oidutil', 94 | 'storetest', 95 | 'test_accept', 96 | 'test_association', 97 | 'test_discover', 98 | 'test_fetchers', 99 | 'test_htmldiscover', 100 | 'test_nonce', 101 | 'test_openidyadis', 102 | 'test_parsehtml', 103 | 'test_urinorm', 104 | 'test_yadis_discover', 105 | 'trustroot', 106 | ] 107 | 108 | loader = unittest.TestLoader() 109 | suite = unittest.TestSuite() 110 | 111 | for m in test_modules: 112 | suite.addTest(loader.loadTestsFromModule(m)) 113 | 114 | for name in custom_module_names: 115 | mod = __import__('openid.test.{}'.format(name), {}, {}, ['unused']) 116 | try: 117 | suite.addTest(mod.pyUnitTests()) 118 | except AttributeError: 119 | # because the AttributeError doesn't actually say which 120 | # object it was. 121 | print(("Error loading tests from %s:" % (name, ))) 122 | raise 123 | 124 | return suite 125 | 126 | 127 | def _import_djopenid(): 128 | """ 129 | Import djopenid from the examples directory without putting it in sys.path 130 | permanently (which we don't really want to do as we don't want namespace 131 | conflicts) 132 | """ 133 | # Find our way to the examples/djopenid directory 134 | grandParentDir = os.path.join(__file__, "..", "..", "..") 135 | grandParentDir = os.path.abspath(grandParentDir) 136 | examplesDir = os.path.join(grandParentDir, "examples") 137 | 138 | sys.path.append(examplesDir) 139 | import djopenid 140 | sys.path.remove(examplesDir) 141 | 142 | 143 | def djangoExampleTests(): 144 | """ 145 | Run tests from examples/djopenid. 146 | 147 | @return: number of failed tests. 148 | """ 149 | # Django uses this to find out where its settings are. 150 | os.environ['DJANGO_SETTINGS_MODULE'] = 'djopenid.settings' 151 | 152 | _import_djopenid() 153 | 154 | try: 155 | import django.test.simple 156 | except ImportError: 157 | raise unittest.SkipTest("Skipping django examples. " 158 | "django.test.simple not found.") 159 | 160 | import djopenid.server.models 161 | import djopenid.consumer.models 162 | print("Testing Django examples:") 163 | 164 | runner = django.test.simple.DjangoTestSuiteRunner() 165 | return runner.run_tests(['server', 'consumer']) 166 | 167 | # These tests do get put into a test suite, so we could run them with the 168 | # other tests, but django also establishes a test database for them, so we 169 | # let it do that thing instead. 170 | return django.test.simple.run_tests( 171 | [djopenid.server.models, djopenid.consumer.models]) 172 | 173 | 174 | def test_suite(): 175 | """ 176 | Collect all of the tests together in a single suite. 177 | """ 178 | addParentToPath() 179 | combined_suite = unittest.TestSuite() 180 | combined_suite.addTests(specialCaseTests()) 181 | combined_suite.addTests(pyUnitTests()) 182 | combined_suite.addTest(unittest.FunctionTestCase(djangoExampleTests)) 183 | return combined_suite 184 | -------------------------------------------------------------------------------- /openid/test/cryptutil.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import random 3 | import os.path 4 | 5 | from openid import cryptutil 6 | 7 | # Most of the purpose of this test is to make sure that cryptutil can 8 | # find a good source of randomness on this machine. 9 | 10 | 11 | def test_cryptrand(): 12 | # It's possible, but HIGHLY unlikely that a correct implementation 13 | # will fail by returning the same number twice 14 | 15 | s = cryptutil.getBytes(32) 16 | t = cryptutil.getBytes(32) 17 | assert len(s) == 32 18 | assert len(t) == 32 19 | assert s != t 20 | 21 | a = cryptutil.randrange(2**128) 22 | b = cryptutil.randrange(2**128) 23 | assert type(a) is int 24 | assert type(b) is int 25 | assert b != a 26 | 27 | # Make sure that we can generate random numbers that are larger 28 | # than platform int size 29 | cryptutil.randrange(int(sys.maxsize) + 1) 30 | 31 | 32 | def test_reversed(): 33 | if hasattr(cryptutil, 'reversed'): 34 | cases = [ 35 | ('', ''), 36 | ('a', 'a'), 37 | ('ab', 'ba'), 38 | ('abc', 'cba'), 39 | ('abcdefg', 'gfedcba'), 40 | ([], []), 41 | ([1], [1]), 42 | ([1, 2], [2, 1]), 43 | ([1, 2, 3], [3, 2, 1]), 44 | (list(range(1000)), list(range(999, -1, -1))), 45 | ] 46 | 47 | for case, expected in cases: 48 | expected = list(expected) 49 | actual = list(cryptutil.reversed(case)) 50 | assert actual == expected, (case, expected, actual) 51 | twice = list(cryptutil.reversed(actual)) 52 | assert twice == list(case), (actual, case, twice) 53 | 54 | 55 | def test_binaryLongConvert(): 56 | MAX = sys.maxsize 57 | for iteration in range(500): 58 | n = 0 59 | for i in range(10): 60 | n += int(random.randrange(MAX)) 61 | 62 | s = cryptutil.longToBinary(n) 63 | assert isinstance(s, bytes) 64 | n_prime = cryptutil.binaryToLong(s) 65 | assert n == n_prime, (n, n_prime) 66 | 67 | cases = [(b'\x00', 0), (b'\x01', 1), (b'\x7F', 127), (b'\x00\xFF', 255), 68 | (b'\x00\x80', 128), (b'\x00\x81', 129), (b'\x00\x80\x00', 32768), 69 | (b'OpenID is cool', 1611215304203901150134421257416556)] 70 | 71 | for s, n in cases: 72 | n_prime = cryptutil.binaryToLong(s) 73 | s_prime = cryptutil.longToBinary(n) 74 | assert n == n_prime, (s, n, n_prime) 75 | assert s == s_prime, (n, s, s_prime) 76 | 77 | 78 | def test_longToBase64(): 79 | f = open(os.path.join(os.path.dirname(__file__), 'n2b64')) 80 | try: 81 | for line in f: 82 | parts = line.strip().split(' ') 83 | p0 = bytes(parts[0], encoding="utf-8") 84 | p1 = cryptutil.longToBase64(int(parts[1])) 85 | assert p0 == p1, (p0, p1, parts) 86 | finally: 87 | f.close() 88 | 89 | 90 | def test_base64ToLong(): 91 | f = open(os.path.join(os.path.dirname(__file__), 'n2b64')) 92 | try: 93 | for line in f: 94 | parts = line.strip().split(' ') 95 | assert int(parts[1]) == cryptutil.base64ToLong(parts[0]) 96 | finally: 97 | f.close() 98 | 99 | 100 | def test(): 101 | test_reversed() 102 | test_binaryLongConvert() 103 | test_cryptrand() 104 | test_longToBase64() 105 | test_base64ToLong() 106 | 107 | 108 | if __name__ == '__main__': 109 | test() 110 | -------------------------------------------------------------------------------- /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/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/openid-1.2-consumer-sqlitestore.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/necaris/python3-openid/fbc7f6a2dc5159b890119a044215a6aee4cefc8d/openid/test/data/openid-1.2-consumer-sqlitestore.db -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /openid/test/data/test1-parsehtml.txt: -------------------------------------------------------------------------------- 1 | found 2 | 3 | 4 | 5 | found 6 | 7 | 8 | 9 | found 10 | 11 | 12 | 13 | found 14 | 15 | 16 | 17 | found 18 | 19 | 20 | 21 | found 22 | 23 | 24 | 25 | found 26 | 27 | 28 | 29 | found 30 | 31 | 32 | 33 | EOF 34 | 35 |