././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8785362
python3-openid-3.2.0/ 0000755 0001750 0001750 00000000000 00000000000 014277 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/LICENSE 0000644 0001750 0001750 00000026136 00000000000 015314 0 ustar 00rami rami 0000000 0000000
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/MANIFEST.in 0000644 0001750 0001750 00000000451 00000000000 016035 0 ustar 00rami rami 0000000 0000000 include LICENSE NOTICE CHANGELOG MANIFEST.in NEWS.md background-associations.txt
graft admin
graft contrib
recursive-include examples README.md discover *.py *.html *.xml
recursive-include openid/test *.txt dhpriv n2b64 *.py
recursive-include openid/test/data *
recursive-include doc *.css *.html
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/NEWS.md 0000644 0001750 0001750 00000020523 00000000000 015377 0 ustar 00rami rami 0000000 0000000 What's New in Python3-OpenID 3.0.2
=================================
All the tests that require no more than the standard library pass -- however
modules that depend on external packages (such as the pyCURL fetcher or the
MySQL / PostgreSQL stores) haven't been updated and may not work.
What's New in Python3-OpenID 3.0.1
=================================
This implementation of OpenID has been ported to Python 3 -- all but one test
is known to pass.
What's New in Python OpenID 2.1.0
=================================
This implementation of OpenID has been upgraded to support version 2.0
of the OpenID Authentication specification.
New in this version is:
* Verification of relying party return_to addresses, to screen out RPs
hiding behind open redirect relays. Server code can invoke this with
the returnToVerified method on CheckIDRequest.
* Helper module for the Provider Authentication Policy Extension (PAPE) in
openid.extensions.pape.
* Helper module for Attribute Exchange in openid.extensions.ax.
Bugfixes:
* Allow the use of lxml as an ElementTree implemenation.
* Provide compatability with a wider range of versions for SQL stores.
Upgrading from 2.0.1
--------------------
The third argument to Consumer.complete() is required.
The sreg module should be imported from openid.extensions.sreg instead of
openid.sreg.
The ax module should likewise be imported from openid.extensions.ax
instead of openid.ax
The openid.extensions.ax.FetchRequest.fromOpenIDRequest method now
takes a CheckIDRequest object instead of a Message object
The OpenID response (the result of Consumer.complete()) now has a
getDisplayIdentifier() method which should be called instead of
accessing response.identity_url. The value of getDisplayIdentifier()
will be the XRI i-name if XRI is used. The value of
response.identity_url SHOULD, however, be used as the application's
database key for storing account information.
What's New in Python OpenID 2.0
===============================
The big news here is compatibility with svn revision 313 of the OpenID 2.0
draft specification.
Highlights include:
* Simple Registration support in a new module openid.sreg. (Those
previously using SuccessResponse.extensionResponse are advised to
look here.)
* OpenID provider-driven identifier selection.
* "Negotiators" allow you to define which association types to use.
* Examples for Django.
Dependencies
------------
Python 2.5 is now supported. Support for Python 2.2 discontinued.
Seperate installation of yadis and urljr packages is no longer
required; they have been included in this package.
Upgrading from 1.1 or 1.2
-------------------------
One of the additions to the OpenID protocol was a specified nonce
format for one-way nonces. As a result, the nonce table in the store
has changed. You'll need to run contrib/upgrade-store-1.1-to-2.0 to
upgrade your store, or you'll encounter errors about the wrong number
of columns in the oid_nonces table.
If you've written your own custom store or code that interacts directly with it,
you'll need to review the change notes in openid.store.interface.
Consumers should now pass an additional parameter to Consumer.complete()
to defend against return_to tampering.
What's New in Python OpenID 1.1.2
=================================
i-name Support
--------------
This version of the library allows the use of XRI as OpenID identifiers,
allowing users to log in with their i-names. For full XRI compatibility,
relying parties integrating this library should take note of the user's
CanonicalID, as described in the "Identifying the End User" section of the
OpenID 2.0 specification.
Bug Fixes
---------
A variety of bug fixes were included in this release, mostly relating to
international issues such as dealing with other character sets, Unicode,
incorrectly flagging certain Norwegian trust roots as suspect, and operation
of the filesystem-backed store on exotic platforms.
Dependencies
------------
* urljr 1.0.1
* yadis 1.1.0
What's New in Python OpenID 1.1.0
=================================
Version 1.1 of the Python OpenID library implements recent changes to
the OpenID specification as well as making API changes that should
make integration with applications easier.
Yadis Support
-------------
One of the major changes to OpenID since the last release has been the
approval of Yadis discovery as the preferred way to specify the OpenID
metadata for an identity URL instead of using tags in
HTML. This library does Yadis discovery, and if that fails, it falls
back to old-style discovery.
Some advantages of Yadis support are:
* Support for fallback if your primary OpenID provider is not available
* Support for load-balancing between OpenID servers
* Easy interoperability for different identity services
For more information about Yadis, see http://yadis.org/
Extension Support
-----------------
OpenID also has formalized support for extensions. Extensions are a
mechanism for transferring information from the consumer to the server
and from the server to the consumer in the process of performing
OpenID authentication. Extensions are implemented as additional
namespaced query arguments that go along with standard OpenID requests
and responses. This library provides a simple API for adding extension
arguments to requests and extracting extension responses from replies.
Dependencies
------------
These dependencies should be available from wherever you acquired the
OpenID library.
* urljr - The fetcher abstraction from the previous OpenID library
has been extended and is also used for the Yadis library. Because
the Yadis library is useful without the OpenID library, the HTTP
fetching code has been rolled into its own package. Additionally,
the library now has the concept of a default fetcher to make APIs
simpler.
* yadis - The Yadis library provides a general discovery layer that
has been adopted by OpenID as well as other identity-related
protocols. Most OpenID identity URLs will work without the Yadis
library, but as time goes on, this library will be more and more
important.
Consumer API
------------
The consumer API has been changed for more natural use as well as to
support extension arguments.
* OpenIDConsumer(store, [fetcher], [immediate]) is now
Consumer(session, store)
- The session object is a dictionary-like object that should be
tied to the requesting HTTP agent, for example, using a session
ID cookie. It is used for Yadis fallback and holding the state
of the OpenID transaction between the redirect to the server
and the response. The values that are placed in the session are
namespaced, so there should not be a conflict with other uses
of the same session. The session namespace is an attribute of
the Consumer object.
- Because the consumer object now does session management, it is
necessary to construct a new consumer object for every
request. Creating consumer objects is light-weight.
* OpenIDConsumer.beginAuth(user_url) is now Consumer.begin(user_url)
and either returns an AuthRequest object or raises an
exception. There is no more tuple unpacking or status codes.
* OpenIDConsumer.constructRedirect(authreq, return_to, trust_root) is
now AuthRequest.redirectURL(trust_root, return_to, [immediate]).
* OpenIDConsumer.completeAuth(token, query) is now
Consumer.complete(query). It no longer returns a tuple. Instead it
returns an object that has a status code and additional information
about the response. See the API documentation for more information.
Server API
----------
The server API has been changed for greater extensibility. Instead
of taking an "is_authorized" callback, processing happens in several
stages, allowing you to insert extension data into the response
before it is signed and returned. See the documentation for the
openid.server.server module.
Fetcher API
-----------
* fetcher was openid.consumer.fetchers.OpenIDHTTPFetcher, is now
urljr.fetchers.HTTPFetcher. get() and post() have been replaced by
fetch(), see urljr.fetchers for details.
Upgrading from 1.0
------------------
The server changed the way it indexes associations in the store, so if
you're upgrading a server installation, we recommend you clear the old
records from your store when you do so. As a consequence, consumers
will re-establish associations with your server a little sooner than
they would have otherwise.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8785362
python3-openid-3.2.0/PKG-INFO 0000644 0001750 0001750 00000002670 00000000000 015401 0 ustar 00rami rami 0000000 0000000 Metadata-Version: 2.1
Name: python3-openid
Version: 3.2.0
Summary: OpenID support for modern servers and consumers.
Home-page: http://github.com/necaris/python3-openid
Author: Rami Chowdhury
Author-email: rami.chowdhury@gmail.com
Maintainer: Rami Chowdhury
Maintainer-email: rami.chowdhury@gmail.com
License: UNKNOWN
Download-URL: http://github.com/necaris/python3-openid/tarball/v3.2.0
Description: This is a set of Python packages to support use of
the OpenID decentralized identity system in your application, update to Python
3. Want to enable single sign-on for your web site? Use the openid.consumer
package. Want to run your own OpenID server? Check out openid.server.
Includes example code and support for a variety of storage back-ends.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
Provides-Extra: mysql
Provides-Extra: postgresql
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586716910.0
python3-openid-3.2.0/README.md 0000644 0001750 0001750 00000004004 00000000000 015554 0 ustar 00rami rami 0000000 0000000 _NOTE_: This started out as a fork of the Python OpenID library, with changes
to make it Python 3 compatible. It's now a port of that library, including
cleanups and updates to the code in general.
[](https://travis-ci.org/necaris/python3-openid)
[](https://coveralls.io/github/necaris/python3-openid?branch=master)
# requirements
- Python 3.5+ (tested on CPython 3.5-3.8, and PyPy3 (although some tests may fail on PyPy))
# installation
The recommended way is to install from PyPI with `pip`:
pip install python3-openid
Alternatively, you can run the following command from a source checkout:
python setup.py install
If you want to use MySQL or PostgreSQL storage options, be sure to install
the relevant "extra":
pip install python3-openid[mysql]
# getting started
The library should follow the existing `python-openid` API as closely as possible.
_NOTE_: documentation will be auto-generated as soon as I can figure out how to
update the documentation tools.
_NOTE_: The examples directory includes an example server and consumer
implementation. See the README file in that directory for more
information on running the examples.
# logging
This library offers a logging hook that will record unexpected
conditions that occur in library code. If a condition is recoverable,
the library will recover and issue a log message. If it is not
recoverable, the library will raise an exception. See the
documentation for the `openid.oidutil` module for more on the logging
hook.
# documentation
The documentation in this library is in Epydoc format, which is
detailed at:
http://epydoc.sourceforge.net/
# contact
Bug reports, suggestions, and feature requests are [very welcome](issues)!
There are also the `#python-openid` and `#openid` channels on FreeNode IRC.
# contributors
- @necaris
- @moreati
- @vstoykov
- @earthday
- @bkmgit
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 011451 x ustar 00 0000000 0000000 27 mtime=1593432941.865203
python3-openid-3.2.0/admin/ 0000755 0001750 0001750 00000000000 00000000000 015367 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586712376.0
python3-openid-3.2.0/admin/build_discover_data.py 0000755 0001750 0001750 00000005023 00000000000 021732 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python3
"""
Build a set of YADIS identity URL / service discovery files in
the format for Apache mod_asis -- simple text files containing their
own HTTP headers.
These can then be used as a basis for testing.
"""
import sys
import os.path
import urllib.parse
from openid.test import discoverdata
manifest_header = """\
# This file contains test cases for doing YADIS identity URL and
# service discovery. For each case, there are three URLs. The first
# URL is the user input. The second is the identity URL and the third
# is the URL from which the XRDS document should be read.
#
# The file format is as follows:
# User URL Identity URL XRDS URL
#
# blank lines and lines starting with # should be ignored.
#
# To use this test:
#
# 1. Run your discovery routine on the User URL.
#
# 2. Compare the identity URL returned by the discovery routine to the
# identity URL on that line of the file. It must be an EXACT match.
#
# 3. Do a regular HTTP GET on the XRDS URL. Compare the content that
# was returned by your discovery routine with the content returned
# from that URL. It should also be an exact match.
"""
def buildDiscover(base_url, out_dir):
"""
Convert all files in a directory to apache mod_asis files in
another directory.
"""
test_data = discoverdata.readTests(discoverdata.default_test_file)
def writeTestFile(test_name):
"""Helper to generate an output data file for a given test name."""
template = test_data[test_name]
data = discoverdata.fillTemplate(test_name, template, base_url,
discoverdata.example_xrds)
out_file_name = os.path.join(out_dir, test_name)
with open(out_file_name, 'w', encoding="utf-8") as out_file:
out_file.write(data)
manifest = [manifest_header]
for success, input_name, id_name, result_name in discoverdata.testlist:
if not success:
continue
writeTestFile(input_name)
input_url = urllib.parse.urljoin(base_url, input_name)
id_url = urllib.parse.urljoin(base_url, id_name)
result_url = urllib.parse.urljoin(base_url, result_name)
manifest.append('\t'.join((input_url, id_url, result_url)))
manifest.append('\n')
manifest_file_name = os.path.join(out_dir, 'manifest.txt')
with open(manifest_file_name, 'w', encoding="utf-8") as manifest_file:
for chunk in manifest:
manifest_file.write(chunk)
if __name__ == '__main__':
buildDiscover(*sys.argv[1:])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/admin/get_tlds.py 0000644 0001750 0001750 00000003165 00000000000 017553 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python3
"""
Fetch the current TLD list from the IANA Web site, parse it, and print
an expression suitable for direct insertion into each library's trust
root validation module.
Usage:
python gettlds.py (php|python|ruby)
Then cut-n-paste.
"""
import urllib.request
import sys
LANGS = {
'php': (
r"'/\.(", # prefix
"'", # line prefix
"|", # separator
"|' .", # line suffix
r")\.?$/'" # suffix
),
'python': ("['", "'", "', '", "',", "']"),
'ruby': ("%w'", "", " ", "", "'"),
}
if __name__ == '__main__':
lang = sys.argv[1]
prefix, line_prefix, separator, line_suffix, suffix = LANGS[lang]
iana_url = 'http://data.iana.org/TLD/tlds-alpha-by-domain.txt'
with urllib.request.urlopen(iana_url) as iana_resource:
tlds = []
output_line = "" # initialize a line of output
for input_line in iana_resource:
if input_line.startswith(b'#'): # skip comments
continue
tld = input_line.decode("utf-8").strip().lower()
nxt_output_line = output_line + prefix + tld # update current line
if len(nxt_output_line) > 60:
# Long enough -- print it and reinitialize to only hold the
# most recent TLD
print(output_line + line_suffix)
output_line = line_prefix + tld
else:
# Not long enough, so update it to the concatenated version
output_line = nxt_output_line
prefix = separator
# Print the final line of remaining output
print(output_line + suffix)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/admin/next_version.py 0000644 0001750 0001750 00000001425 00000000000 020466 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python3
"""
Compute the next release version of the library, using `--major`, `--minor`,
or `--patch` arguments to determine the level at which the version is to be
incremented.
"""
import sys
from os.path import abspath, join, dirname
if __name__ == '__main__':
sys.path.append(abspath(join(dirname(__file__), '..')))
import openid
major, minor, patch = openid.version_info
pieces = None
if '--major' in sys.argv:
pieces = (major + 1, 0, 0)
elif '--minor' in sys.argv:
pieces = (major, minor + 1, 0)
elif '--patch' in sys.argv:
pieces = (major, minor, patch + 1)
if pieces:
print('.'.join(map(str, pieces)), end='')
else:
print('Major, minor, or patch?', file=sys.stderr)
sys.exit(1)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/admin/patch_version.py 0000644 0001750 0001750 00000001547 00000000000 020614 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python3
"""
Update the `version_info` embedded in the library to the given version.
"""
import sys
from os.path import abspath, join, dirname
if __name__ == '__main__':
try:
major, minor, patch = map(int, sys.argv[1].split('.'))
except (IndexError, ValueError):
print('Need version string in form MAJOR.MINOR.PATCH', file=sys.stderr)
sys.exit(1)
TARGET = abspath(join(dirname(__file__), '..', 'openid', '__init__.py'))
with open(TARGET, 'r', encoding='utf8') as f:
lines = f.readlines()
for i, l in enumerate(lines):
if l.startswith('version_info'):
v_info = '({}, {}, {})'.format(major, minor, patch)
lines[i] = 'version_info = {}\n\n'.format(v_info)
break
with open(TARGET, 'w', encoding='utf8') as f:
f.writelines(lines)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/background-associations.txt 0000644 0001750 0001750 00000010416 00000000000 021656 0 ustar 00rami rami 0000000 0000000 Background association requests
###############################
This document describes how to make signing in with OpenID faster for
users of your application by never making the users wait for an
association to be made, but using associations when they're
available. Most OpenID libraries and applications attempt to make
associations during the discovery phase of the OpenID authentication
request. Because association requests may have to do Diffie-Hellman
key exchange, which is time consuming. Even if Diffie-Hellman key
exchange is not used, the user still needs to wait for the association
request.
Setting up your application to make associations in the background
==================================================================
When making associations background, there are two components that
need access to the OpenID association store: the consumer application
and the background association fetcher. The consumer needs to be set
up to record the server URL for any request for which an association
does not exist or is expired instead of making a new association. The
background fetcher looks at the server URL queue and makes
associations for any server URLs that need them. After the
associations are made, the consumer will use them until they expire
again. While associations are expired or missing, the consumer will
use stateless mode to complete authentications with the servers that
need associations.
The OpenID server endpoint URL queue
-----------------------------------------------------------------
You will have to set up a conduit between the consumer and the
background association fetcher so that the background association
fetcher knows what servers need associations. The background
association fetcher will not fetch associations for servers that
already have them, so the queue does not have to be very smart. It
could be as simple as a file to which the server URLs are
appended. Either way, the queue needs to be write-able by the consumer
and readable by the background fetcher.
Configuring the consumer
-----------------------------------------------------------------
Create a subclass of ``GenericConsumer`` that overrides
``_negotiateAssociation`` so that it just records the server URL that
needs an association::
from openid.consumer.consumer import GenericConsumer, Consumer
class LazyAssociationConsumer(GenericConsumer):
needs_assoc_file = None
def _negotiateAssociation(self, endpoint):
# Do whatever you need to do here to send the server_url to
# the queue. This example just appends it to a file.
self.needs_assoc_file.write(endpoint.server_url + '\n')
self.needs_assoc_file.flush()
You could also store the whole endpoint object. When you instantiate
the consumer, pass this generic consumer class to the controlling
consumer::
return Consumer(session, store, consumer_class=LazyAssociationConsumer)
The background association fetcher
-----------------------------------------------------------------
The background association fetcher is just a script that should be
added to ``cron`` or triggered periodically. If you are ambitious, you
could make the background fetcher listen for inserts into the queue.
The background fetcher needs to do something similar to the following::
def refresh(consumer, endpoint):
if consumer.store.getAssociation(endpoint.server_url):
logger.info("We don't need to associate with %r", endpoint.server_url)
return
logger.info("Associating with %r", endpoint.server_url)
now = time.time()
assoc = consumer._negotiateAssociation(endpoint)
if assoc:
elapsed = time.time() - now
logger.info('(%0.2f seconds) Associated with %r', elapsed,
endpoint.server_url)
consumer.store.storeAssociation(endpoint.server_url, assoc)
else:
logger.error('Failed to make an association with %r',
endpoint.server_url)
The code in this example logs the amount of time that the association
request took. This is time that the user would have been waiting. The
``consumer`` in this example is a standard consumer, not the
``LazyAssociationConsumer`` that was defined in the section
above. This is important, because the lazy consumer above will not
actually make any associations.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/contrib/ 0000755 0001750 0001750 00000000000 00000000000 015737 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/contrib/associate 0000755 0001750 0001750 00000002604 00000000000 017642 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python
"""Make an OpenID Assocition request against an endpoint
and print the results."""
import sys
from openid.store.memstore import MemoryStore
from openid.consumer import consumer
from openid.consumer.discover import OpenIDServiceEndpoint
from datetime import datetime
def verboseAssociation(assoc):
"""A more verbose representation of an Association.
"""
d = assoc.__dict__
issued_date = datetime.fromtimestamp(assoc.issued)
d['issued_iso'] = issued_date.isoformat()
fmt = """ Type: %(assoc_type)s
Handle: %(handle)s
Issued: %(issued)s [%(issued_iso)s]
Lifetime: %(lifetime)s
Secret: %(secret)r
"""
return fmt % d
def main():
if not sys.argv[1:]:
print("Usage: %s ENDPOINT_URL..." % (sys.argv[0],))
for endpoint_url in sys.argv[1:]:
print("Associating with", endpoint_url)
# This makes it clear why j3h made AssociationManager when we
# did the ruby port. We can't invoke requestAssociation
# without these other trappings.
store = MemoryStore()
endpoint = OpenIDServiceEndpoint()
endpoint.server_url = endpoint_url
c = consumer.GenericConsumer(store)
auth_req = c.begin(endpoint)
if auth_req.assoc:
print(verboseAssociation(auth_req.assoc))
else:
print(" ...no association.")
if __name__ == '__main__':
main()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/contrib/openid-parse 0000644 0001750 0001750 00000010212 00000000000 020244 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python
"""Grab URLs from the clipboard, interpret the queries as OpenID, and print.
In addition to URLs, I also scan for queries as they appear in httpd log files,
with a pattern like 'GET /foo?bar=baz HTTP'.
Requires the 'xsel' program to get the contents of the clipboard.
"""
from pprint import pformat
from urllib.parse import urlsplit, urlunsplit
import cgi, re, subprocess, sys
from openid import message
OPENID_SORT_ORDER = ['mode', 'identity', 'claimed_id']
class NoQuery(Exception):
def __init__(self, url):
self.url = url
def __str__(self):
return "No query in url %s" % (self.url, )
def getClipboard():
xsel = subprocess.Popen(["xsel", "-o", "-b"], stdout=subprocess.PIPE)
output = xsel.communicate()[0]
return output
def main():
source = getClipboard()
urls = find_urls(source)
errors = []
output = []
queries = []
queries.extend(queriesFromPostdata(source))
for url in urls:
try:
queries.append(queryFromURL(url))
except NoQuery as err:
errors.append(err)
queries.extend(queriesFromLogs(source))
for where, query in queries:
output.append('at %s:\n%s' % (where, openidFromQuery(query)))
if output:
print('\n\n'.join(output))
elif errors:
for err in errors:
print(err)
def queryFromURL(url):
split_url = urlsplit(url)
query = cgi.parse_qs(split_url[3])
if not query:
raise NoQuery(url)
url_without_query = urlunsplit(split_url[:3] + (None, None))
return (url_without_query, query)
def openidFromQuery(query):
try:
msg = message.Message.fromPostArgs(unlistify(query))
s = formatOpenIDMessage(msg)
except Exception as err:
# XXX - side effect.
sys.stderr.write(str(err))
s = pformat(query)
return s
def formatOpenIDMessage(msg):
value_lists = {}
for (ns_uri, ns_key), value in msg.args.items():
l = value_lists.setdefault(ns_uri, {})
l[ns_key] = value
output = []
for ns_uri, values in value_lists.items():
ns_output = []
alias = msg.namespaces.getAlias(ns_uri)
if alias is message.NULL_NAMESPACE:
alias = 'openid'
ns_output.append(" %s <%s>" % (alias, ns_uri))
for key in OPENID_SORT_ORDER:
try:
ns_output.append(" %s = %s" % (key, values.pop(key)))
except KeyError:
pass
values = values.items()
values.sort()
for k, v in values:
ns_output.append(" %s = %s" % (k, v))
output.append('\n'.join(ns_output))
return '\n\n'.join(output)
def unlistify(d):
return dict((i[0], i[1][0]) for i in d.items())
def queriesFromLogs(s):
qre = re.compile(r'GET (/.*)?\?(.+) HTTP')
return [(match.group(1), cgi.parse_qs(match.group(2)))
for match in qre.finditer(s)]
def queriesFromPostdata(s):
# This looks for query data in a line that starts POSTDATA=.
# Tamperdata outputs such lines. If there's a 'Host=' in that block,
# use that too, but don't require it.
qre = re.compile(r'(?:^Host=(?P.+?)$.*?)?^POSTDATA=(?P.*)$',
re.DOTALL | re.MULTILINE)
return [(match.group('host') or 'POSTDATA',
cgi.parse_qs(match.group('query'))) for match in qre.finditer(s)]
def find_urls(s):
# Regular expression borrowed from urlscan
# by Daniel Burrows , GPL.
urlinternalpattern = r'[{}a-zA-Z/\-_0-9%?&.=:;+,#~]'
urltrailingpattern = r'[{}a-zA-Z/\-_0-9%&=+#]'
httpurlpattern = r'(?:https?://' + urlinternalpattern + r'*' + urltrailingpattern + r')'
# Used to guess that blah.blah.blah.TLD is a URL.
tlds = ['biz', 'com', 'edu', 'info', 'org']
guessedurlpattern = r'(?:[a-zA-Z0-9_\-%]+(?:\.[a-zA-Z0-9_\-%]+)*\.(?:' + '|'.join(
tlds) + '))'
urlre = re.compile(
r'(?:<(?:URL:)?)?(' + httpurlpattern + '|' + guessedurlpattern +
'|(?:mailto:[a-zA-Z0-9\-_]*@[0-9a-zA-Z_\-.]*[0-9a-zA-Z_\-]))>?')
return [match.group(1) for match in urlre.finditer(s)]
if __name__ == '__main__':
main()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/contrib/upgrade-store-1.1-to-2.0 0000644 0001750 0001750 00000013270 00000000000 021660 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python
# SQL Store Upgrade Script
# for version 1.x to 2.0 of the OpenID library.
# Doesn't depend on the openid library, so you can run this python
# script to update databases for ruby or PHP as well.
#
# Testers note:
#
# A SQLite3 db with the 1.2 schema exists in
# openid/test/data/openid-1.2-consumer-sqlitestore.db if you want something
# to try upgrading.
#
# TODO:
# * test data for mysql and postgresql.
# * automated tests.
import os
import getpass
import sys
from optparse import OptionParser
def askForPassword():
return getpass.getpass("DB Password: ")
def askForConfirmation(dbname, tablename):
print """The table %s from the database %s will be dropped, and
an empty table with the new nonce table schema will replace it.""" % (
tablename, dbname)
return raw_input("Continue? ").lower().strip().startswith('y')
def doSQLiteUpgrade(db_conn, nonce_table_name='oid_nonces'):
cur = db_conn.cursor()
cur.execute('DROP TABLE %s' % nonce_table_name)
sql = """
CREATE TABLE %s (
server_url VARCHAR,
timestamp INTEGER,
salt CHAR(40),
UNIQUE(server_url, timestamp, salt)
);
""" % nonce_table_name
cur.execute(sql)
cur.close()
def doMySQLUpgrade(db_conn, nonce_table_name='oid_nonces'):
cur = db_conn.cursor()
cur.execute('DROP TABLE %s' % nonce_table_name)
sql = """
CREATE TABLE %s (
server_url BLOB,
timestamp INTEGER,
salt CHAR(40),
PRIMARY KEY (server_url(255), timestamp, salt)
)
TYPE=InnoDB;
""" % nonce_table_name
cur.execute(sql)
cur.close()
def doPostgreSQLUpgrade(db_conn, nonce_table_name='oid_nonces'):
cur = db_conn.cursor()
cur.execute('DROP TABLE %s' % nonce_table_name)
sql = """
CREATE TABLE %s (
server_url VARCHAR(2047),
timestamp INTEGER,
salt CHAR(40),
PRIMARY KEY (server_url, timestamp, salt)
);
""" % nonce_table_name
cur.execute(sql)
cur.close()
db_conn.commit()
def main(argv=None):
parser = OptionParser()
parser.add_option(
"-u",
"--user",
dest="username",
default=os.environ.get('USER'),
help="User name to use to connect to the DB. "
"Defaults to USER environment variable.")
parser.add_option(
'-t',
'--table',
dest='tablename',
default='oid_nonces',
help='The name of the nonce table to drop and recreate. '
' Defaults to "oid_nonces", the default table name for '
'the openid stores.')
parser.add_option(
'--mysql',
dest='mysql_db_name',
help='Upgrade a table from this MySQL database. '
'Requires username for database.')
parser.add_option(
'--pg',
'--postgresql',
dest='postgres_db_name',
help='Upgrade a table from this PostgreSQL database. '
'Requires username for database.')
parser.add_option(
'--sqlite',
dest='sqlite_db_name',
help='Upgrade a table from this SQLite database file.')
parser.add_option(
'--host',
dest='db_host',
default='localhost',
help='Host on which to find MySQL or PostgreSQL DB.')
(options, args) = parser.parse_args(argv)
db_conn = None
if options.sqlite_db_name:
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
print "You must have pysqlite2 installed in your PYTHONPATH."
return 1
try:
db_conn = sqlite.connect(options.sqlite_db_name)
except Exception, e:
print "Could not connect to SQLite database:", str(e)
return 1
if askForConfirmation(options.sqlite_db_name, options.tablename):
doSQLiteUpgrade(db_conn, nonce_table_name=options.tablename)
if options.postgres_db_name:
if not options.username:
print "A username is required to open a PostgreSQL Database."
return 1
password = askForPassword()
try:
import psycopg2
except ImportError:
try:
from psycopg2cffi import compat
compat.register()
except:
print "You need psycopg2 installed to update a postgres DB."
return 1
try:
db_conn = psycopg2.connect(
database=options.postgres_db_name,
user=options.username,
host=options.db_host,
password=password)
except Exception, e:
print "Could not connect to PostgreSQL database:", str(e)
return 1
if askForConfirmation(options.postgres_db_name, options.tablename):
doPostgreSQLUpgrade(db_conn, nonce_table_name=options.tablename)
if options.mysql_db_name:
if not options.username:
print "A username is required to open a MySQL Database."
return 1
password = askForPassword()
try:
import MySQLdb
except ImportError:
print "You must have MySQLdb installed to update a MySQL DB."
return 1
try:
db_conn = MySQLdb.connect(options.db_host, options.username,
password, options.mysql_db_name)
except Exception, e:
print "Could not connect to MySQL database:", str(e)
return 1
if askForConfirmation(options.mysql_db_name, options.tablename):
doMySQLUpgrade(db_conn, nonce_table_name=options.tablename)
if db_conn:
db_conn.close()
else:
parser.print_help()
return 0
if __name__ == '__main__':
retval = main()
sys.exit(retval)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/examples/ 0000755 0001750 0001750 00000000000 00000000000 016115 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/__init__.py 0000644 0001750 0001750 00000000000 00000000000 020214 0 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586716910.0
python3-openid-3.2.0/examples/consumer.py 0000644 0001750 0001750 00000045377 00000000000 020342 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python
"""
Simple example for an OpenID consumer.
Once you understand this example you'll know the basics of OpenID
and using the Python OpenID library. You can then move on to more
robust examples, and integrating OpenID into your application.
"""
__copyright__ = 'Copyright 2005-2008, Janrain, Inc.'
from http.cookies import SimpleCookie
import html
import urllib.parse
import cgitb
import sys
def quoteattr(s):
qs = html.escape(s, 1)
return '"%s"' % (qs, )
from http.server import HTTPServer, BaseHTTPRequestHandler
try:
import openid
except ImportError:
sys.stderr.write("""
Failed to import the OpenID library. In order to use this example, you
must either install the library (see INSTALL in the root of the
distribution) or else add the library to python's import path (the
PYTHONPATH environment variable).
For more information, see the README in the root of the library
distribution.""")
sys.exit(1)
from openid.store import memstore
from openid.store import filestore
from openid.consumer import consumer
from openid.oidutil import appendArgs
from openid.cryptutil import randomString
from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
from openid.extensions import pape, sreg
from random import randrange
# Used with an OpenID provider affiliate program.
OPENID_PROVIDER_NAME = 'MyOpenID'
OPENID_PROVIDER_URL = 'https://www.myopenid.com/affiliate_signup?affiliate_id=39'
class OpenIDHTTPServer(HTTPServer):
"""http server that contains a reference to an OpenID consumer and
knows its base URL.
"""
def __init__(self, store, *args, **kwargs):
HTTPServer.__init__(self, *args, **kwargs)
self.sessions = {}
self.store = store
if self.server_port != 80:
self.base_url = ('http://%s:%s/' %
(self.server_name, self.server_port))
else:
self.base_url = 'http://%s/' % (self.server_name, )
class OpenIDRequestHandler(BaseHTTPRequestHandler):
"""Request handler that knows how to verify an OpenID identity."""
SESSION_COOKIE_NAME = 'pyoidconsexsid'
session = None
def getConsumer(self, stateless=False):
if stateless:
store = None
else:
store = self.server.store
return consumer.Consumer(self.getSession(), store)
def getSession(self):
"""Return the existing session or a new session"""
if self.session is not None:
return self.session
# Get value of cookie header that was sent
cookie_str = self.headers.get('Cookie')
if cookie_str:
cookie_obj = SimpleCookie(cookie_str)
sid_morsel = cookie_obj.get(self.SESSION_COOKIE_NAME, None)
if sid_morsel is not None:
sid = sid_morsel.value
else:
sid = None
else:
sid = None
# If a session id was not set, create a new one
if sid is None:
sid = randomString(16, '0123456789abcdef')
session = None
else:
session = self.server.sessions.get(sid)
# If no session exists for this session ID, create one
if session is None:
session = self.server.sessions[sid] = {}
session['id'] = sid
self.session = session
return session
def setSessionCookie(self):
sid = self.getSession()['id']
session_cookie = '%s=%s;' % (self.SESSION_COOKIE_NAME, sid)
self.send_header('Set-Cookie', session_cookie)
def do_GET(self):
"""Dispatching logic. There are three paths defined:
/ - Display an empty form asking for an identity URL to
verify
/verify - Handle form submission, initiating OpenID verification
/process - Handle a redirect from an OpenID server
Any other path gets a 404 response. This function also parses
the query parameters.
If an exception occurs in this function, a traceback is
written to the requesting browser.
"""
try:
self.parsed_uri = urllib.parse.urlparse(self.path)
self.query = {}
for k, v in urllib.parse.parse_qsl(self.parsed_uri[4]):
self.query[k] = v
path = self.parsed_uri[2]
if path == '/':
self.render()
elif path == '/verify':
self.doVerify()
elif path == '/process':
self.doProcess()
elif path == '/affiliate':
self.doAffiliate()
else:
self.notFound()
except (KeyboardInterrupt, SystemExit):
raise
except:
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.setSessionCookie()
self.end_headers()
self.wfile.write(
bytes(cgitb.html(sys.exc_info(), context=10), 'utf-8'))
def doVerify(self):
"""Process the form submission, initating OpenID verification.
"""
# First, make sure that the user entered something
openid_url = self.query.get('openid_identifier')
if not openid_url:
self.render(
'Enter an OpenID Identifier to verify.',
css_class='error',
form_contents=openid_url)
return
immediate = 'immediate' in self.query
use_sreg = 'use_sreg' in self.query
use_pape = 'use_pape' in self.query
use_stateless = 'use_stateless' in self.query
oidconsumer = self.getConsumer(stateless=use_stateless)
try:
request = oidconsumer.begin(openid_url)
except consumer.DiscoveryFailure as exc:
fetch_error_string = 'Error in discovery: %s' % (
html.escape(str(exc)))
self.render(
fetch_error_string,
css_class='error',
form_contents=openid_url)
else:
if request is None:
msg = 'No OpenID services found for %s' % (
html.escape(openid_url), )
self.render(msg, css_class='error', form_contents=openid_url)
else:
# Then, ask the library to begin the authorization.
# Here we find out the identity server that will verify the
# user's identity, and get a token that allows us to
# communicate securely with the identity server.
if use_sreg:
self.requestRegistrationData(request)
if use_pape:
self.requestPAPEDetails(request)
trust_root = self.server.base_url
return_to = self.buildURL('process')
if request.shouldSendRedirect():
redirect_url = request.redirectURL(
trust_root, return_to, immediate=immediate)
self.send_response(302)
self.send_header('Location', redirect_url)
self.writeUserHeader()
self.end_headers()
else:
form_html = request.htmlMarkup(
trust_root,
return_to,
form_tag_attrs={'id': 'openid_message'},
immediate=immediate)
self.wfile.write(bytes(form_html, 'utf-8'))
def requestRegistrationData(self, request):
sreg_request = sreg.SRegRequest(
required=['nickname'], optional=['fullname', 'email'])
request.addExtension(sreg_request)
def requestPAPEDetails(self, request):
pape_request = pape.Request([pape.AUTH_PHISHING_RESISTANT])
request.addExtension(pape_request)
def doProcess(self):
"""Handle the redirect from the OpenID server.
"""
oidconsumer = self.getConsumer()
# Ask the library to check the response that the server sent
# us. Status is a code indicating the response type. info is
# either None or a string containing more information about
# the return type.
url = 'http://' + self.headers.get('Host') + self.path
info = oidconsumer.complete(self.query, url)
sreg_resp = None
pape_resp = None
css_class = 'error'
display_identifier = info.getDisplayIdentifier()
if info.status == consumer.FAILURE and display_identifier:
# In the case of failure, if info is non-None, it is the
# URL that we were verifying. We include it in the error
# message to help the user figure out what happened.
fmt = "Verification of %s failed: %s"
message = fmt % (html.escape(display_identifier), info.message)
elif info.status == consumer.SUCCESS:
# Success means that the transaction completed without
# error. If info is None, it means that the user cancelled
# the verification.
css_class = 'alert'
# This is a successful verification attempt. If this
# was a real application, we would do our login,
# comment posting, etc. here.
fmt = "You have successfully verified %s as your identity."
message = fmt % (html.escape(display_identifier), )
sreg_resp = sreg.SRegResponse.fromSuccessResponse(info)
pape_resp = pape.Response.fromSuccessResponse(info)
if info.endpoint.canonicalID:
# You should authorize i-name users by their canonicalID,
# rather than their more human-friendly identifiers. That
# way their account with you is not compromised if their
# i-name registration expires and is bought by someone else.
message += (" This is an i-name, and its persistent ID is %s"
% (html.escape(info.endpoint.canonicalID), ))
elif info.status == consumer.CANCEL:
# cancelled
message = 'Verification cancelled'
elif info.status == consumer.SETUP_NEEDED:
if info.setup_url:
message = 'Setup needed' % (
quoteattr(info.setup_url), )
else:
# This means auth didn't succeed, but you're welcome to try
# non-immediate mode.
message = 'Setup needed'
else:
# Either we don't understand the code or there is no
# openid_url included with the error. Give a generic
# failure message. The library should supply debug
# information in a log.
message = 'Verification failed.'
self.render(
message,
css_class,
display_identifier,
sreg_data=sreg_resp,
pape_data=pape_resp)
def doAffiliate(self):
"""Direct the user sign up with an affiliate OpenID provider."""
sreg_req = sreg.SRegRequest(['nickname'], ['fullname', 'email'])
href = sreg_req.toMessage().toURL(OPENID_PROVIDER_URL)
message = """Get an OpenID at %s""" % (
quoteattr(href), OPENID_PROVIDER_NAME)
self.render(message)
def renderSREG(self, sreg_data):
if not sreg_data:
self.wfile.write(
bytes(
'
', 'utf-8'))
def renderPAPE(self, pape_data):
if not pape_data:
self.wfile.write(
bytes('
No PAPE data was returned
',
'utf-8'))
else:
self.wfile.write(
bytes('
Effective Auth Policies
',
'utf-8'))
for policy_uri in pape_data.auth_policies:
self.wfile.write(
bytes('
%s
' % (html.escape(policy_uri), ),
'utf-8'))
if not pape_data.auth_policies:
self.wfile.write(
bytes('
No policies were applied.
', 'utf-8'))
self.wfile.write(bytes('
', 'utf-8'))
def buildURL(self, action, **query):
"""Build a URL relative to the server base_url, with the given
query parameters added."""
base = urllib.parse.urljoin(self.server.base_url, action)
return appendArgs(base, query)
def notFound(self):
"""Render a page with a 404 return code and a message."""
fmt = 'The path %s was not understood by this server.'
msg = fmt % (self.path, )
openid_url = self.query.get('openid_identifier')
self.render(msg, 'error', openid_url, status=404)
def render(self,
message=None,
css_class='alert',
form_contents=None,
status=200,
title="Python OpenID Consumer Example",
sreg_data=None,
pape_data=None):
"""Render a page."""
self.send_response(status)
self.send_header("Content-type", "text/html")
self.end_headers()
self.pageHeader(title)
if message:
self.wfile.write(
("
".encode('utf-8'))
if sreg_data is not None:
self.renderSREG(sreg_data)
if pape_data is not None:
self.renderPAPE(pape_data)
self.pageFooter(form_contents)
def pageHeader(self, title):
"""Render the page header"""
self.setSessionCookie()
self.wfile.write(
bytes('''
%s
%s
This example consumer uses the Python
OpenID library. It just verifies that the identifier that you enter
is your identifier.
''' % (title, title), 'UTF-8'))
def pageFooter(self, form_contents):
"""Render the page footer"""
if not form_contents:
form_contents = ''
self.wfile.write(
bytes('''\
''' % (quoteattr(self.buildURL('verify')), quoteattr(form_contents)), 'UTF-8'))
def main(host, port, data_path, weak_ssl=False):
# Instantiate OpenID consumer store and OpenID consumer. If you
# were connecting to a database, you would create the database
# connection and instantiate an appropriate store here.
if data_path:
store = filestore.FileOpenIDStore(data_path)
else:
store = memstore.MemoryStore()
if weak_ssl:
setDefaultFetcher(Urllib2Fetcher())
addr = (host, port)
server = OpenIDHTTPServer(store, addr, OpenIDRequestHandler)
print('Server running at:')
print(server.base_url)
server.serve_forever()
if __name__ == '__main__':
host = 'localhost'
port = 8001
weak_ssl = False
try:
import optparse
except ImportError:
pass # Use defaults (for Python 2.2)
else:
parser = optparse.OptionParser('Usage:\n %prog [options]')
parser.add_option(
'-d',
'--data-path',
dest='data_path',
help='Data directory for storing OpenID consumer state. '
'Setting this option implies using a "FileStore."')
parser.add_option(
'-p',
'--port',
dest='port',
type='int',
default=port,
help='Port on which to listen for HTTP requests. '
'Defaults to port %default.')
parser.add_option(
'-s',
'--host',
dest='host',
default=host,
help='Host on which to listen for HTTP requests. '
'Also used for generating URLs. Defaults to %default.')
parser.add_option(
'-w',
'--weakssl',
dest='weakssl',
default=False,
action='store_true',
help='Skip ssl cert verification')
options, args = parser.parse_args()
if args:
parser.error('Expected no arguments. Got %r' % args)
host = options.host
port = options.port
data_path = options.data_path
weak_ssl = options.weakssl
main(host, port, data_path, weak_ssl)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/discover 0000644 0001750 0001750 00000002535 00000000000 017663 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python
from openid.consumer.discover import discover, DiscoveryFailure
from openid.fetchers import HTTPFetchingError
names = [
["server_url", "Server URL "],
["local_id", "Local ID "],
["canonicalID", "Canonical ID"],
]
def show_services(user_input, normalized, services):
print " Claimed identifier:", normalized
if services:
print " Discovered OpenID services:"
for n, service in enumerate(services):
print " %s." % (n, )
for attr, name in names:
val = getattr(service, attr, None)
if val is not None:
print " %s: %s" % (name, val)
print " Type URIs:"
for type_uri in service.type_uris:
print " *", type_uri
print
else:
print " No OpenID services found"
print
if __name__ == "__main__":
import sys
for user_input in sys.argv[1:]:
print "=" * 50
print "Running discovery on", user_input
try:
normalized, services = discover(user_input)
except DiscoveryFailure, why:
print "Discovery failed:", why
print
except HTTPFetchingError, why:
print "HTTP request failed:", why
print
else:
show_services(user_input, normalized, services)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/examples/djopenid/ 0000755 0001750 0001750 00000000000 00000000000 017711 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/__init__.py 0000644 0001750 0001750 00000000000 00000000000 022010 0 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/examples/djopenid/consumer/ 0000755 0001750 0001750 00000000000 00000000000 021544 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/consumer/__init__.py 0000644 0001750 0001750 00000000000 00000000000 023643 0 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/consumer/models.py 0000644 0001750 0001750 00000000071 00000000000 023377 0 ustar 00rami rami 0000000 0000000 from django.db import models
# Create your models here.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/consumer/urls.py 0000644 0001750 0001750 00000000276 00000000000 023110 0 ustar 00rami rami 0000000 0000000 from django.conf.urls.defaults import *
urlpatterns = patterns(
'djopenid.consumer.views',
(r'^$', 'startOpenID'),
(r'^finish/$', 'finishOpenID'),
(r'^xrds/$', 'rpXRDS'), )
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/consumer/views.py 0000644 0001750 0001750 00000017761 00000000000 023267 0 ustar 00rami rami 0000000 0000000 from django import http
from django.http import HttpResponseRedirect
from django.views.generic.base import TemplateView
from openid.consumer import consumer
from openid.consumer.discover import DiscoveryFailure
from openid.extensions import ax, pape, sreg
from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE
from openid.server.trustroot import RP_RETURN_TO_URL_TYPE
from djopenid import util
PAPE_POLICIES = [
'AUTH_PHISHING_RESISTANT',
'AUTH_MULTI_FACTOR',
'AUTH_MULTI_FACTOR_PHYSICAL',
]
# List of (name, uri) for use in generating the request form.
POLICY_PAIRS = [(p, getattr(pape, p)) for p in PAPE_POLICIES]
def getOpenIDStore():
"""
Return an OpenID store object fit for the currently-chosen
database backend, if any.
"""
return util.getOpenIDStore('/tmp/djopenid_c_store', 'c_')
def getConsumer(request):
"""
Get a Consumer object to perform OpenID authentication.
"""
return consumer.Consumer(request.session, getOpenIDStore())
def renderIndexPage(request, **template_args):
template_args['consumer_url'] = util.getViewURL(request, startOpenID)
template_args['pape_policies'] = POLICY_PAIRS
response = TemplateView(request, 'consumer/index.html', template_args)
response[YADIS_HEADER_NAME] = util.getViewURL(request, rpXRDS)
return response
def startOpenID(request):
"""
Start the OpenID authentication process. Renders an
authentication form and accepts its POST.
* Renders an error message if OpenID cannot be initiated
* Requests some Simple Registration data using the OpenID
library's Simple Registration machinery
* Generates the appropriate trust root and return URL values for
this application (tweak where appropriate)
* Generates the appropriate redirect based on the OpenID protocol
version.
"""
if request.POST:
# Start OpenID authentication.
openid_url = request.POST['openid_identifier']
c = getConsumer(request)
error = None
try:
auth_request = c.begin(openid_url)
except DiscoveryFailure as e:
# Some other protocol-level failure occurred.
error = "OpenID discovery error: %s" % (str(e), )
if error:
# Render the page with an error.
return renderIndexPage(request, error=error)
# Add Simple Registration request information. Some fields
# are optional, some are required. It's possible that the
# server doesn't support sreg or won't return any of the
# fields.
sreg_request = sreg.SRegRequest(
optional=['email', 'nickname'], required=['dob'])
auth_request.addExtension(sreg_request)
# Add Attribute Exchange request information.
ax_request = ax.FetchRequest()
# XXX - uses myOpenID-compatible schema values, which are
# not those listed at axschema.org.
ax_request.add(
ax.AttrInfo('http://schema.openid.net/namePerson', required=True))
ax_request.add(
ax.AttrInfo(
'http://schema.openid.net/contact/web/default',
required=False,
count=ax.UNLIMITED_VALUES))
auth_request.addExtension(ax_request)
# Add PAPE request information. We'll ask for
# phishing-resistant auth and display any policies we get in
# the response.
requested_policies = []
policy_prefix = 'policy_'
for k, v in request.POST.items():
if k.startswith(policy_prefix):
policy_attr = k[len(policy_prefix):]
if policy_attr in PAPE_POLICIES:
requested_policies.append(getattr(pape, policy_attr))
if requested_policies:
pape_request = pape.Request(requested_policies)
auth_request.addExtension(pape_request)
# Compute the trust root and return URL values to build the
# redirect information.
trust_root = util.getViewURL(request, startOpenID)
return_to = util.getViewURL(request, finishOpenID)
# Send the browser to the server either by sending a redirect
# URL or by generating a POST form.
if auth_request.shouldSendRedirect():
url = auth_request.redirectURL(trust_root, return_to)
return HttpResponseRedirect(url)
else:
# Beware: this renders a template whose content is a form
# and some javascript to submit it upon page load. Non-JS
# users will have to click the form submit button to
# initiate OpenID authentication.
form_id = 'openid_message'
form_html = auth_request.formMarkup(trust_root, return_to, False,
{'id': form_id})
return TemplateView(request, 'consumer/request_form.html',
{'html': form_html})
return renderIndexPage(request)
def finishOpenID(request):
"""
Finish the OpenID authentication process. Invoke the OpenID
library with the response from the OpenID server and render a page
detailing the result.
"""
result = {}
# Because the object containing the query parameters is a
# MultiValueDict and the OpenID library doesn't allow that, we'll
# convert it to a normal dict.
# OpenID 2 can send arguments as either POST body or GET query
# parameters.
request_args = util.normalDict(request.GET)
if request.method == 'POST':
request_args.update(util.normalDict(request.POST))
if request_args:
c = getConsumer(request)
# Get a response object indicating the result of the OpenID
# protocol.
return_to = util.getViewURL(request, finishOpenID)
response = c.complete(request_args, return_to)
# Get a Simple Registration response object if response
# information was included in the OpenID response.
sreg_response = {}
ax_items = {}
if response.status == consumer.SUCCESS:
sreg_response = sreg.SRegResponse.fromSuccessResponse(response)
ax_response = ax.FetchResponse.fromSuccessResponse(response)
if ax_response:
ax_items = {
'fullname':
ax_response.get('http://schema.openid.net/namePerson'),
'web':
ax_response.get(
'http://schema.openid.net/contact/web/default'),
}
# Get a PAPE response object if response information was
# included in the OpenID response.
pape_response = None
if response.status == consumer.SUCCESS:
pape_response = pape.Response.fromSuccessResponse(response)
if not pape_response.auth_policies:
pape_response = None
# Map different consumer status codes to template contexts.
results = {
consumer.CANCEL: {
'message': 'OpenID authentication cancelled.'
},
consumer.FAILURE: {
'error': 'OpenID authentication failed.'
},
consumer.SUCCESS: {
'url': response.getDisplayIdentifier(),
'sreg': sreg_response and list(sreg_response.items()),
'ax': list(ax_items.items()),
'pape': pape_response
}
}
result = results[response.status]
if isinstance(response, consumer.FailureResponse):
# In a real application, this information should be
# written to a log for debugging/tracking OpenID
# authentication failures. In general, the messages are
# not user-friendly, but intended for developers.
result['failure_reason'] = response.message
return renderIndexPage(request, **result)
def rpXRDS(request):
"""
Return a relying party verification XRDS document
"""
return util.renderXRDS(request, [RP_RETURN_TO_URL_TYPE],
[util.getViewURL(request, finishOpenID)])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/manage.py 0000644 0001750 0001750 00000001064 00000000000 021514 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write(
"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"
% __file__)
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/examples/djopenid/server/ 0000755 0001750 0001750 00000000000 00000000000 021217 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/server/__init__.py 0000644 0001750 0001750 00000000000 00000000000 023316 0 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/server/models.py 0000644 0001750 0001750 00000000071 00000000000 023052 0 ustar 00rami rami 0000000 0000000 from django.db import models
# Create your models here.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/server/tests.py 0000644 0001750 0001750 00000007121 00000000000 022734 0 ustar 00rami rami 0000000 0000000 from django.test.testcases import TestCase
from djopenid.server import views
from djopenid import util
from django.http import HttpRequest
from django.contrib.sessions.backends.cache import SessionStore
from openid.server.server import CheckIDRequest
from openid.message import Message
from openid.yadis.constants import YADIS_CONTENT_TYPE
from openid.yadis.services import applyFilter
def dummyRequest():
request = HttpRequest()
request.session = SessionStore()
request.META['HTTP_HOST'] = 'example.invalid'
request.META['SERVER_PROTOCOL'] = 'HTTP'
return request
class TestProcessTrustResult(TestCase):
def setUp(self):
self.request = dummyRequest()
id_url = util.getViewURL(self.request, views.idPage)
# Set up the OpenID request we're responding to.
op_endpoint = 'http://127.0.0.1:8080/endpoint'
message = Message.fromPostArgs({
'openid.mode':
'checkid_setup',
'openid.identity':
id_url,
'openid.return_to':
'http://127.0.0.1/%s' % (self.id(), ),
'openid.sreg.required':
'postcode',
})
self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint)
views.setRequest(self.request, self.openid_request)
def test_allow(self):
self.request.POST['allow'] = 'Yes'
response = views.processTrustResult(self.request)
self.assertEqual(response.status_code, 302)
finalURL = response['location']
self.assertIn('openid.mode=id_res', finalURL)
self.assertIn('openid.identity=', finalURL)
self.assertIn('openid.sreg.postcode=12345', finalURL)
def test_cancel(self):
self.request.POST['cancel'] = 'Yes'
response = views.processTrustResult(self.request)
self.assertEqual(response.status_code, 302)
finalURL = response['location']
self.assertIn('openid.mode=cancel', finalURL)
self.assertNotIn('openid.identity=', finalURL)
self.assertNotIn('openid.sreg.postcode=12345', finalURL)
class TestShowDecidePage(TestCase):
def test_unreachableRealm(self):
self.request = dummyRequest()
id_url = util.getViewURL(self.request, views.idPage)
# Set up the OpenID request we're responding to.
op_endpoint = 'http://127.0.0.1:8080/endpoint'
message = Message.fromPostArgs({
'openid.mode':
'checkid_setup',
'openid.identity':
id_url,
'openid.return_to':
'http://unreachable.invalid/%s' % (self.id(), ),
'openid.sreg.required':
'postcode',
})
self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint)
views.setRequest(self.request, self.openid_request)
response = views.showDecidePage(self.request, self.openid_request)
self.assertContains(response, 'trust_root_valid is Unreachable')
class TestGenericXRDS(TestCase):
def test_genericRender(self):
"""
Render XRDS document with a single type URI and a single endpoint URL
Parse it to see that it matches.
"""
request = dummyRequest()
type_uris = ['A_TYPE']
endpoint_url = 'A_URL'
response = util.renderXRDS(request, type_uris, [endpoint_url])
requested_url = 'http://requested.invalid/'
(endpoint, ) = applyFilter(requested_url, response.content)
self.assertEqual(YADIS_CONTENT_TYPE, response['Content-Type'])
self.assertEqual(type_uris, endpoint.type_uris)
self.assertEqual(endpoint_url, endpoint.uri)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/server/urls.py 0000644 0001750 0001750 00000000450 00000000000 022555 0 ustar 00rami rami 0000000 0000000 from django.conf.urls.defaults import *
urlpatterns = patterns(
'djopenid.server.views',
(r'^$', 'server'),
(r'^xrds/$', 'idpXrds'),
(r'^processTrustResult/$', 'processTrustResult'),
(r'^user/$', 'idPage'),
(r'^endpoint/$', 'endpoint'),
(r'^trust/$', 'trustPage'), )
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/server/views.py 0000644 0001750 0001750 00000022453 00000000000 022734 0 ustar 00rami rami 0000000 0000000 """
This module implements an example server for the OpenID library. Some
functionality has been omitted intentionally; this code is intended to
be instructive on the use of this library. This server does not
perform actual user authentication and serves up only one OpenID URL,
with the exception of IDP-generated identifiers.
Some code conventions used here:
* 'request' is a Django request object.
* 'openid_request' is an OpenID library request object.
* 'openid_response' is an OpenID library response
"""
import cgi
from djopenid import util
from djopenid.util import getViewURL
from django import http
from django.template import RequestContext
from django.shortcuts import render_to_response
from openid.server.server import Server, ProtocolError, CheckIDRequest, \
EncodingError
from openid.server.trustroot import verifyReturnTo
from openid.yadis.discover import DiscoveryFailure
from openid.consumer.discover import OPENID_IDP_2_0_TYPE
from openid.extensions import sreg
from openid.extensions import pape
from openid.fetchers import HTTPFetchingError
def getOpenIDStore():
"""
Return an OpenID store object fit for the currently-chosen
database backend, if any.
"""
return util.getOpenIDStore('/tmp/djopenid_s_store', 's_')
def getServer(request):
"""
Get a Server object to perform OpenID authentication.
"""
return Server(getOpenIDStore(), getViewURL(request, endpoint))
def setRequest(request, openid_request):
"""
Store the openid request information in the session.
"""
if openid_request:
request.session['openid_request'] = openid_request
else:
request.session['openid_request'] = None
def getRequest(request):
"""
Get an openid request from the session, if any.
"""
return request.session.get('openid_request')
def server(request):
"""
Respond to requests for the server's primary web page.
"""
return render_to_response(
'server/index.html', {
'user_url': getViewURL(request, idPage),
'server_xrds_url': getViewURL(request, idpXrds),
},
context_instance=RequestContext(request))
def idpXrds(request):
"""
Respond to requests for the IDP's XRDS document, which is used in
IDP-driven identifier selection.
"""
return util.renderXRDS(request, [OPENID_IDP_2_0_TYPE],
[getViewURL(request, endpoint)])
def idPage(request):
"""
Serve the identity page for OpenID URLs.
"""
return render_to_response(
'server/idPage.html', {'server_url': getViewURL(request, endpoint)},
context_instance=RequestContext(request))
def trustPage(request):
"""
Display the trust page template, which allows the user to decide
whether to approve the OpenID verification.
"""
return render_to_response(
'server/trust.html',
{'trust_handler_url': getViewURL(request, processTrustResult)},
context_instance=RequestContext(request))
def endpoint(request):
"""
Respond to low-level OpenID protocol messages.
"""
s = getServer(request)
query = util.normalDict(request.GET or request.POST)
# First, decode the incoming request into something the OpenID
# library can use.
try:
openid_request = s.decodeRequest(query)
except ProtocolError as why:
# This means the incoming request was invalid.
return render_to_response(
'server/endpoint.html', {'error': str(why)},
context_instance=RequestContext(request))
# If we did not get a request, display text indicating that this
# is an endpoint.
if openid_request is None:
return render_to_response(
'server/endpoint.html', {},
context_instance=RequestContext(request))
# We got a request; if the mode is checkid_*, we will handle it by
# getting feedback from the user or by checking the session.
if openid_request.mode in ["checkid_immediate", "checkid_setup"]:
return handleCheckIDRequest(request, openid_request)
else:
# We got some other kind of OpenID request, so we let the
# server handle this.
openid_response = s.handleRequest(openid_request)
return displayResponse(request, openid_response)
def handleCheckIDRequest(request, openid_request):
"""
Handle checkid_* requests. Get input from the user to find out
whether she trusts the RP involved. Possibly, get intput about
what Simple Registration information, if any, to send in the
response.
"""
# If the request was an IDP-driven identifier selection request
# (i.e., the IDP URL was entered at the RP), then return the
# default identity URL for this server. In a full-featured
# provider, there could be interaction with the user to determine
# what URL should be sent.
if not openid_request.idSelect():
id_url = getViewURL(request, idPage)
# Confirm that this server can actually vouch for that
# identifier
if id_url != openid_request.identity:
# Return an error response
error_response = ProtocolError(
openid_request.message, "This server cannot verify the URL %r"
% (openid_request.identity, ))
return displayResponse(request, error_response)
if openid_request.immediate:
# Always respond with 'cancel' to immediate mode requests
# because we don't track information about a logged-in user.
# If we did, then the answer would depend on whether that user
# had trusted the request's trust root and whether the user is
# even logged in.
openid_response = openid_request.answer(False)
return displayResponse(request, openid_response)
else:
# Store the incoming request object in the session so we can
# get to it later.
setRequest(request, openid_request)
return showDecidePage(request, openid_request)
def showDecidePage(request, openid_request):
"""
Render a page to the user so a trust decision can be made.
@type openid_request: openid.server.server.CheckIDRequest
"""
trust_root = openid_request.trust_root
return_to = openid_request.return_to
try:
# Stringify because template's ifequal can only compare to strings.
trust_root_valid = verifyReturnTo(trust_root, return_to) \
and "Valid" or "Invalid"
except DiscoveryFailure as err:
trust_root_valid = "DISCOVERY_FAILED"
except HTTPFetchingError as err:
trust_root_valid = "Unreachable"
pape_request = pape.Request.fromOpenIDRequest(openid_request)
return render_to_response(
'server/trust.html', {
'trust_root': trust_root,
'trust_handler_url': getViewURL(request, processTrustResult),
'trust_root_valid': trust_root_valid,
'pape_request': pape_request,
},
context_instance=RequestContext(request))
def processTrustResult(request):
"""
Handle the result of a trust decision and respond to the RP
accordingly.
"""
# Get the request from the session so we can construct the
# appropriate response.
openid_request = getRequest(request)
# The identifier that this server can vouch for
response_identity = getViewURL(request, idPage)
# If the decision was to allow the verification, respond
# accordingly.
allowed = 'allow' in request.POST
# Generate a response with the appropriate answer.
openid_response = openid_request.answer(
allowed, identity=response_identity)
# Send Simple Registration data in the response, if appropriate.
if allowed:
sreg_data = {
'fullname': 'Example User',
'nickname': 'example',
'dob': '1970-01-01',
'email': 'invalid@example.com',
'gender': 'F',
'postcode': '12345',
'country': 'ES',
'language': 'eu',
'timezone': 'America/New_York',
}
sreg_req = sreg.SRegRequest.fromOpenIDRequest(openid_request)
sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
openid_response.addExtension(sreg_resp)
pape_response = pape.Response()
pape_response.setAuthLevel(pape.LEVELS_NIST, 0)
openid_response.addExtension(pape_response)
return displayResponse(request, openid_response)
def displayResponse(request, openid_response):
"""
Display an OpenID response. Errors will be displayed directly to
the user; successful responses and other protocol-level messages
will be sent using the proper mechanism (i.e., direct response,
redirection, etc.).
"""
s = getServer(request)
# Encode the response into something that is renderable.
try:
webresponse = s.encodeResponse(openid_response)
except EncodingError as why:
# If it couldn't be encoded, display an error.
text = why.response.encodeToKVForm()
return render_to_response(
'server/endpoint.html', {'error': cgi.escape(text)},
context_instance=RequestContext(request))
# Construct the appropriate django framework response.
r = http.HttpResponse(webresponse.body)
r.status_code = webresponse.code
for header, value in webresponse.headers.items():
r[header] = value
return r
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/settings.py 0000644 0001750 0001750 00000004511 00000000000 022124 0 ustar 00rami rami 0000000 0000000 """
Django settings for djopenid example project
"""
import os
import sys
import warnings
try:
import openid
except ImportError as e:
warnings.warn(
"Could not import OpenID. Please consult the djopenid README.")
sys.exit(1)
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = ( # ('Your Name', 'your_email@domain.com'),
)
MANAGERS = ADMINS
# NOTE that this is a sample configuration and probably not suitable for
# production use in any way, shape or form.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': './sqlite.db'
}
}
# Local time zone for this installation. All choices can be found here:
# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
TIME_ZONE = 'Europe/London'
# Language code for this installation. All choices can be found here:
# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
# http://blogs.law.harvard.edu/tech/stories/storyReader$15
LANGUAGE_CODE = 'en-us.UTF-8'
USE_I18N = False
SITE_ID = 1
# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT.
# Example: "http://media.lawrence.com"
MEDIA_URL = ''
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/'
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'u^bw6lmsa6fah0$^lz-ct$)y7x7#ag92-z+y45-8!(jk0lkavy'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = ('django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader', )
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.doc.XViewMiddleware', )
ROOT_URLCONF = 'djopenid.urls'
TEMPLATE_CONTEXT_PROCESSORS = ()
TEMPLATE_DIRS = (
os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates')), )
INSTALLED_APPS = (
'django.contrib.contenttypes',
'django.contrib.sessions',
# These are the example OpenID consumer and server
'djopenid.consumer',
'djopenid.server', )
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/examples/djopenid/templates/ 0000755 0001750 0001750 00000000000 00000000000 021707 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/examples/djopenid/templates/consumer/ 0000755 0001750 0001750 00000000000 00000000000 023542 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/templates/consumer/index.html 0000644 0001750 0001750 00000005406 00000000000 025544 0 ustar 00rami rami 0000000 0000000
Django OpenID Example Consumer
This is an example consumer built for the Django framework. Enter
an OpenID in the box below.
{% if error %}
{{ error|escape }}
{% endif %}
{% if url %}
OpenID authentication succeeded; you authenticated as
{{ url|escape }}.
{% if sreg %}
Simple Registration data returned:
{% for pair in sreg %}
{{ pair.0 }}: {{ pair.1 }}
{% endfor %}
{% else %}
The server returned no Simple Registration data.
{% endif %}
{% if ax %}
Attribute Exchange data returned:
{% for pair in ax %}
{{ pair.0 }}: {{ pair.1|join:", " }}
{% endfor %}
{% else %}
The server returned no Attribute Exchange data.
{% endif %}
{% if pape %}
An authentication policy response contained these policies:
{% for uri in pape.auth_policies %}
{{ uri }}
{% endfor %}
{% else %}
The server returned no authentication policy data (PAPE).
{% endif %}
This is a Django package which implements both an OpenID server
and an OpenID consumer. These examples are provided with the
OpenID library so you can learn how to use it in your own
applications.
This is the identity page for the OpenID that this server serves.
{% endblock %}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/templates/server/index.html 0000644 0001750 0001750 00000002032 00000000000 025207 0 ustar 00rami rami 0000000 0000000
Django OpenID Example Server
{% block head %}
{% endblock %}
{% block body %}
This is an example server built for the Django framework. It only
authenticates one OpenID, which is also served by this
application. The OpenID it serves is
{% endblock %}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/templates/server/pape_request_info.html 0000644 0001750 0001750 00000001045 00000000000 027613 0 ustar 00rami rami 0000000 0000000 {% if pape_request %}
{% if pape_request.preferred_auth_policies %}
The relying party requested the following PAPE policies be in effect:
{% for uri in pape_request.preferred_auth_policies %}
{{ uri }}
{% endfor %}
{% endif %}
{% if pape_request.preferred_auth_level_types %}
The relying party requested the following authentication level types:
{% for uri in pape_request.preferred_auth_level_types %}
This request claims to be from {{ trust_root|escape }} but I have
determined that it is a pack of lies. Beware, if you release
information to them, they are likely to do unconscionable things with it,
being the lying liars that they are.
Please tell the real {{ trust_root|escape }} that someone is
trying to abuse your trust in their good name.
The site {{ trust_root|escape }} has requested verification
of your OpenID. I have failed to reach it and thus cannot vouch for its
authenticity. Perhaps it is on your local network.
The site {{ trust_root|escape }} has requested verification
of your OpenID. However, {{ trust_root|escape }} does not
implement OpenID 2.0's relying party verification mechanism. Please use
extra caution in deciding whether to release information to this party,
and ask {{ trust_root|escape }} to implement relying party
verification for your future transactions.
{% include "server/pape_request_info.html" %}
{% endifequal %}
{% endblock %}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/templates/xrds.xml 0000644 0001750 0001750 00000000571 00000000000 023414 0 ustar 00rami rami 0000000 0000000
{% for type_uri in type_uris %}
{{ type_uri|escape }}
{% endfor %}
{% for endpoint_url in endpoint_urls %}
{{ endpoint_url }}
{% endfor %}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/urls.py 0000644 0001750 0001750 00000000331 00000000000 021245 0 ustar 00rami rami 0000000 0000000 from django.conf.urls.defaults import *
urlpatterns = patterns(
'',
('^$', 'djopenid.views.index'),
('^consumer/', include('djopenid.consumer.urls')),
('^server/', include('djopenid.server.urls')), )
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/util.py 0000644 0001750 0001750 00000011715 00000000000 021245 0 ustar 00rami rami 0000000 0000000 """
Utility code for the Django example consumer and server.
"""
from urllib.parse import urljoin
from django.db import connection
from django.template.context import RequestContext
from django.template import loader
from django import http
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse as reverseURL
from django.shortcuts import render_to_response
from django.conf import settings
try:
import psycopg2
except ImportError:
from psycopg2cffi import compat
compat.register()
from openid.store.filestore import FileOpenIDStore
from openid.store import sqlstore
from openid.yadis.constants import YADIS_CONTENT_TYPE
def getOpenIDStore(filestore_path, table_prefix):
"""
Returns an OpenID association store object based on the database
engine chosen for this Django application.
* If no database engine is chosen, a filesystem-based store will
be used whose path is filestore_path.
* If a database engine is chosen, a store object for that database
type will be returned.
* If the chosen engine is not supported by the OpenID library,
raise ImproperlyConfigured.
* If a database store is used, this will create the tables
necessary to use it. The table names will be prefixed with
table_prefix. DO NOT use the same table prefix for both an
OpenID consumer and an OpenID server in the same database.
The result of this function should be passed to the Consumer
constructor as the store parameter.
"""
db_engine = settings.DATABASES['default']['ENGINE']
if not db_engine:
return FileOpenIDStore(filestore_path)
# Possible side-effect: create a database connection if one isn't
# already open.
connection.cursor()
# Create table names to specify for SQL-backed stores.
tablenames = {
'associations_table': table_prefix + 'openid_associations',
'nonces_table': table_prefix + 'openid_nonces',
}
types = {
'django.db.backends.postgresql_psycopg2': sqlstore.PostgreSQLStore,
'django.db.backends.mysql': sqlstore.MySQLStore,
'django.db.backends.sqlite3': sqlstore.SQLiteStore,
}
if db_engine not in types:
raise ImproperlyConfigured(
"Database engine %s not supported by OpenID library" % db_engine)
s = types[db_engine](connection.connection, **tablenames)
try:
s.createTables()
except (SystemExit, KeyboardInterrupt, MemoryError):
raise
except:
# XXX This is not the Right Way to do this, but because the
# underlying database implementation might differ in behavior
# at this point, we can't reliably catch the right
# exception(s) here. Ideally, the SQL store in the OpenID
# library would catch exceptions that it expects and fail
# silently, but that could be bad, too. More ideally, the SQL
# store would not attempt to create tables it knows already
# exists.
pass
return s
def getViewURL(req, view_name_or_obj, args=None, kwargs=None):
relative_url = reverseURL(view_name_or_obj, args=args, kwargs=kwargs)
full_path = req.META.get('SCRIPT_NAME', '') + relative_url
return urljoin(getBaseURL(req), full_path)
def getBaseURL(req):
"""
Given a Django web request object, returns the OpenID 'trust root'
for that request; namely, the absolute URL to the site root which
is serving the Django request. The trust root will include the
proper scheme and authority. It will lack a port if the port is
standard (80, 443).
"""
name = req.META['HTTP_HOST']
try:
name = name[:name.index(':')]
except:
pass
try:
port = int(req.META['SERVER_PORT'])
except:
port = 80
proto = req.META['SERVER_PROTOCOL']
if 'HTTPS' in proto:
proto = 'https'
else:
proto = 'http'
if port in [80, 443] or not port:
port = ''
else:
port = ':%s' % (port, )
url = "%s://%s%s/" % (proto, name, port)
return url
def normalDict(request_data):
"""
Converts a django request MutliValueDict (e.g., request.GET,
request.POST) into a standard python dict whose values are the
first value from each of the MultiValueDict's value lists. This
avoids the OpenID library's refusal to deal with dicts whose
values are lists, because in OpenID, each key in the query arg set
can have at most one value.
"""
return dict((k, v) for k, v in request_data.items())
def renderXRDS(request, type_uris, endpoint_urls):
"""Render an XRDS page with the specified type URIs and endpoint
URLs in one service block, and return a response with the
appropriate content-type.
"""
response = render_to_response(
'xrds.xml', {'type_uris': type_uris,
'endpoint_urls': endpoint_urls},
context_instance=RequestContext(request))
response['Content-Type'] = YADIS_CONTENT_TYPE
return response
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/examples/djopenid/views.py 0000644 0001750 0001750 00000000643 00000000000 021423 0 ustar 00rami rami 0000000 0000000 from djopenid import util
from django.views.generic.base import TemplateView
def index(request):
consumer_url = util.getViewURL(request,
'djopenid.consumer.views.startOpenID')
server_url = util.getViewURL(request, 'djopenid.server.views.server')
return TemplateView(request, 'index.html', {
'consumer_url': consumer_url,
'server_url': server_url
})
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586716910.0
python3-openid-3.2.0/examples/server.py 0000644 0001750 0001750 00000061611 00000000000 020002 0 ustar 00rami rami 0000000 0000000 #!/usr/bin/env python
__copyright__ = 'Copyright 2005-2008, Janrain, Inc.'
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse
from urllib.parse import parse_qsl
import time
import http.cookies
import html
import cgitb
import sys
def quoteattr(s):
qs = html.escape(s, 1)
return '"%s"' % (qs, )
try:
import openid
except ImportError:
sys.stderr.write("""
Failed to import the OpenID library. In order to use this example, you
must either install the library (see INSTALL in the root of the
distribution) or else add the library to python's import path (the
PYTHONPATH environment variable).
For more information, see the README in the root of the library
distribution.""")
sys.exit(1)
from openid.extensions import sreg
from openid.server import server
from openid.store.filestore import FileOpenIDStore
from openid.consumer import discover
class OpenIDHTTPServer(HTTPServer):
"""
http server that contains a reference to an OpenID Server and
knows its base URL.
"""
def __init__(self, *args, **kwargs):
HTTPServer.__init__(self, *args, **kwargs)
if self.server_port != 80:
self.base_url = ('http://%s:%s/' %
(self.server_name, self.server_port))
else:
self.base_url = 'http://%s/' % (self.server_name, )
self.openid = None
self.approved = {}
self.lastCheckIDRequest = {}
def setOpenIDServer(self, oidserver):
self.openid = oidserver
class ServerHandler(BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
self.user = None
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
def do_GET(self):
try:
self.parsed_uri = urlparse(self.path)
self.query = {}
for k, v in parse_qsl(self.parsed_uri[4]):
self.query[k] = v
self.setUser()
path = self.parsed_uri[2].lower()
if path == '/':
self.showMainPage()
elif path == '/openidserver':
self.serverEndPoint(self.query)
elif path == '/login':
self.showLoginPage('/', '/')
elif path == '/loginsubmit':
self.doLogin()
elif path.startswith('/id/'):
self.showIdPage(path)
elif path.startswith('/yadis/'):
self.showYadis(path[7:])
elif path == '/serveryadis':
self.showServerYadis()
else:
self.send_response(404)
self.end_headers()
except (KeyboardInterrupt, SystemExit):
raise
except:
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(
bytes(cgitb.html(sys.exc_info(), context=10), 'utf-8'))
def do_POST(self):
try:
self.parsed_uri = urlparse(self.path)
self.setUser()
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
self.query = {}
for k, v in parse_qsl(post_data):
self.query[k] = v
path = self.parsed_uri[2]
if path == '/openidserver':
self.serverEndPoint(self.query)
elif path == '/allow':
self.handleAllow(self.query)
else:
self.send_response(404)
self.end_headers()
except (KeyboardInterrupt, SystemExit):
raise
except:
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(
bytes(cgitb.html(sys.exc_info(), context=10), 'utf-8'))
def handleAllow(self, query):
# pretend this next bit is keying off the user's session or something,
# right?
request = self.server.lastCheckIDRequest.get(self.user)
query = self.binaryToUTF8(query)
if 'yes' in query:
if 'login_as' in query:
self.user = query['login_as']
if request.idSelect():
identity = self.server.base_url + 'id/' + query['identifier']
else:
identity = request.identity
trust_root = request.trust_root
if query.get('remember', 'no') == 'yes':
self.server.approved[(identity, trust_root)] = 'always'
response = self.approved(request, identity)
elif 'no' in query:
response = request.answer(False)
else:
assert False, 'strange allow post. %r' % (query, )
self.displayResponse(response)
def setUser(self):
cookies = self.headers.get('Cookie')
if cookies:
morsel = http.cookies.BaseCookie(cookies).get('user')
if morsel:
self.user = morsel.value
def isAuthorized(self, identity_url, trust_root):
if self.user is None:
return False
if identity_url != self.server.base_url + 'id/' + self.user:
return False
key = (identity_url, trust_root)
return self.server.approved.get(key) is not None
def serverEndPoint(self, query):
try:
query = self.binaryToUTF8(query)
request = self.server.openid.decodeRequest(query)
except server.ProtocolError as why:
self.displayResponse(why)
return
if request is None:
# Display text indicating that this is an endpoint.
self.showAboutPage()
return
if request.mode in ["checkid_immediate", "checkid_setup"]:
self.handleCheckIDRequest(request)
else:
response = self.server.openid.handleRequest(request)
self.displayResponse(response)
def addSRegResponse(self, request, response):
sreg_req = sreg.SRegRequest.fromOpenIDRequest(request)
# In a real application, this data would be user-specific,
# and the user should be asked for permission to release
# it.
sreg_data = {'nickname': self.user}
sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data)
response.addExtension(sreg_resp)
def approved(self, request, identifier=None):
response = request.answer(True, identity=identifier)
self.addSRegResponse(request, response)
return response
def handleCheckIDRequest(self, request):
is_authorized = self.isAuthorized(request.identity, request.trust_root)
if is_authorized:
response = self.approved(request)
self.displayResponse(response)
elif request.immediate:
response = request.answer(False)
self.displayResponse(response)
else:
self.server.lastCheckIDRequest[self.user] = request
self.showDecidePage(request)
def displayResponse(self, response):
try:
webresponse = self.server.openid.encodeResponse(response)
except server.EncodingError as why:
text = why.response.encodeToKVForm()
self.showErrorPage('
%s
' % html.escape(text))
return
self.send_response(webresponse.code)
for header, value in webresponse.headers.items():
self.send_header(header, value)
self.writeUserHeader()
self.end_headers()
if webresponse.body:
self.wfile.write(bytes(webresponse.body, 'utf-8'))
def doLogin(self):
if 'submit' in self.query:
if 'user' in self.query:
self.user = self.query['user']
else:
self.user = None
self.redirect(self.query['success_to'])
elif 'cancel' in self.query:
self.redirect(self.query['fail_to'])
else:
assert 0, 'strange login %r' % (self.query, )
def redirect(self, url):
self.send_response(302)
self.send_header('Location', url)
self.writeUserHeader()
self.end_headers()
def writeUserHeader(self):
if self.user is None:
t1970 = time.gmtime(0)
expires = time.strftime('Expires=%a, %d-%b-%y %H:%M:%S GMT', t1970)
self.send_header('Set-Cookie', 'user=;%s' % expires)
else:
self.send_header('Set-Cookie', 'user=%s' % self.user)
def showAboutPage(self):
endpoint_url = self.server.base_url + 'openidserver'
def link(url):
url_attr = quoteattr(url)
url_text = html.escape(url)
return '%s' % (url_attr, url_text)
def term(url, text):
return '
%s
%s
' % (link(url), text)
resources = [
(self.server.base_url, "This example server's home page"),
('http://www.openidenabled.com/',
'An OpenID community Web site, home of this library'),
('http://www.openid.net/', 'the official OpenID Web site'),
]
resource_markup = ''.join([term(url, text) for url, text in resources])
self.showPage(
200,
'This is an OpenID server',
msg="""\
''' % error_message)
def showDecidePage(self, request):
id_url_base = self.server.base_url + 'id/'
# XXX: This may break if there are any synonyms for id_url_base,
# such as referring to it by IP address or a CNAME.
assert (request.identity.startswith(id_url_base) or
request.idSelect()), repr((request.identity, id_url_base))
expected_user = request.identity[len(id_url_base):]
if request.idSelect(): # We are being asked to select an ID
msg = '''\
A site has asked for your identity. You may select an
identifier by which you would like this site to know you.
On a production site this would likely be a drop down list
of pre-created accounts or have the facility to generate
a random anonymous identifier.
A new site has asked to confirm your identity. If you
approve, the site represented by the trust root below will
be told that you control identity URL listed below. (If
you are using a delegated identity, the site will take
care of reversing the delegation on its own.)
A site has asked for an identity belonging to
%(expected_user)s, but you are logged in as %(user)s. To
log in as %(expected_user)s and approve the login request,
hit OK below. The "Remember this decision" checkbox
applies only to the trust root decision.
To use this server with a consumer, the consumer must be
able to fetch HTTP pages from this web server. If this
computer is behind a firewall, you will not be able to use
OpenID consumers outside of the firewall with it.
You may log in with any name. This server does not use
passwords because it is just a sample of how to use the OpenID
library.
''' % (success_to, fail_to))
def showPage(self,
response_code,
title,
head_extras='',
msg=None,
err=None,
form=None):
if self.user is None:
user_link = 'not logged in.'
else:
user_link = 'logged in as %s. Log out' % \
(self.user, self.user)
body = ''
if err is not None:
body += '''\
%(body)s
''' % contents, 'UTF-8'))
def binaryToUTF8(self, data):
args = {}
for key, value in data.items():
key = key.decode('utf-8')
value = value.decode('utf-8')
args[key] = value
return args
def main(host, port, data_path):
addr = (host, port)
httpserver = OpenIDHTTPServer(addr, ServerHandler)
# Instantiate OpenID consumer store and OpenID consumer. If you
# were connecting to a database, you would create the database
# connection and instantiate an appropriate store here.
store = FileOpenIDStore(data_path)
oidserver = server.Server(store, httpserver.base_url + 'openidserver')
httpserver.setOpenIDServer(oidserver)
print('Server running at:')
print(httpserver.base_url)
httpserver.serve_forever()
if __name__ == '__main__':
host = 'localhost'
data_path = 'sstore'
port = 8000
try:
import optparse
except ImportError:
pass # Use defaults (for Python 2.2)
else:
parser = optparse.OptionParser('Usage:\n %prog [options]')
parser.add_option(
'-d',
'--data-path',
dest='data_path',
default=data_path,
help='Data directory for storing OpenID consumer state. '
'Defaults to "%default" in the current directory.')
parser.add_option(
'-p',
'--port',
dest='port',
type='int',
default=port,
help='Port on which to listen for HTTP requests. '
'Defaults to port %default.')
parser.add_option(
'-s',
'--host',
dest='host',
default=host,
help='Host on which to listen for HTTP requests. '
'Also used for generating URLs. Defaults to %default.')
options, args = parser.parse_args()
if args:
parser.error('Expected no arguments. Got %r' % args)
host = options.host
port = options.port
data_path = options.data_path
main(host, port, data_path)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8685362
python3-openid-3.2.0/openid/ 0000755 0001750 0001750 00000000000 00000000000 015555 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1593432618.0
python3-openid-3.2.0/openid/__init__.py 0000644 0001750 0001750 00000002534 00000000000 017672 0 ustar 00rami rami 0000000 0000000 #-*- coding: utf-8 -*-
"""
This package is an implementation of the OpenID specification in
Python. It contains code for both server and consumer
implementations. For information on implementing an OpenID consumer,
see the C{L{openid.consumer.consumer}} module. For information on
implementing an OpenID server, see the C{L{openid.server.server}}
module.
@contact: U{http://github.com/necaris/python3-openid/}
@copyright: (C) 2005-2008 JanRain, Inc., 2012-2017 Rami Chowdhury
@license: Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
U{http://www.apache.org/licenses/LICENSE-2.0}
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions
and limitations under the License.
"""
version_info = (3, 2, 0)
__version__ = ".".join(str(x) for x in version_info)
__all__ = [
'association',
'consumer',
'cryptutil',
'dh',
'extension',
'extensions',
'fetchers',
'kvform',
'message',
'oidutil',
'server',
'sreg',
'store',
'urinorm',
'yadis',
]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/association.py 0000644 0001750 0001750 00000043576 00000000000 020462 0 ustar 00rami rami 0000000 0000000 #-*-test-case-name: openid.test.test_association-*-
#-*- coding: utf-8 -*-
"""
This module contains code for dealing with associations between
consumers and servers. Associations contain a shared secret that is
used to sign C{openid.mode=id_res} messages.
Users of the library should not usually need to interact directly with
associations. The L{store}, L{server}
and L{consumer} objects will create and manage
the associations. The consumer and server code will make use of a
C{L{SessionNegotiator}} when managing associations, which enables
users to express a preference for what kind of associations should be
allowed, and what kind of exchange should be done to establish the
association.
@var default_negotiator: A C{L{SessionNegotiator}} that allows all
association types that are specified by the OpenID
specification. It prefers to use HMAC-SHA1/DH-SHA1, if it's
available. If HMAC-SHA256 is not supported by your Python runtime,
HMAC-SHA256 and DH-SHA256 will not be available.
@var encrypted_negotiator: A C{L{SessionNegotiator}} that
does not support C{'no-encryption'} associations. It prefers
HMAC-SHA1/DH-SHA1 association types if available.
"""
import time
import functools
from openid import cryptutil
from openid import kvform
from openid import oidutil
from openid.message import OPENID_NS
__all__ = [
'default_negotiator',
'encrypted_negotiator',
'SessionNegotiator',
'Association',
]
all_association_types = [
'HMAC-SHA1',
'HMAC-SHA256',
]
if hasattr(cryptutil, 'hmacSha256'):
supported_association_types = list(all_association_types)
default_association_order = [
('HMAC-SHA1', 'DH-SHA1'),
('HMAC-SHA1', 'no-encryption'),
('HMAC-SHA256', 'DH-SHA256'),
('HMAC-SHA256', 'no-encryption'),
]
only_encrypted_association_order = [
('HMAC-SHA1', 'DH-SHA1'),
('HMAC-SHA256', 'DH-SHA256'),
]
else:
supported_association_types = ['HMAC-SHA1']
default_association_order = [
('HMAC-SHA1', 'DH-SHA1'),
('HMAC-SHA1', 'no-encryption'),
]
only_encrypted_association_order = [
('HMAC-SHA1', 'DH-SHA1'),
]
def getSessionTypes(assoc_type):
"""Return the allowed session types for a given association type"""
assoc_to_session = {
'HMAC-SHA1': ['DH-SHA1', 'no-encryption'],
'HMAC-SHA256': ['DH-SHA256', 'no-encryption'],
}
return assoc_to_session.get(assoc_type, [])
def checkSessionType(assoc_type, session_type):
"""Check to make sure that this pair of assoc type and session
type are allowed"""
if session_type not in getSessionTypes(assoc_type):
raise ValueError('Session type %r not valid for assocation type %r' %
(session_type, assoc_type))
class SessionNegotiator(object):
"""A session negotiator controls the allowed and preferred
association types and association session types. Both the
C{L{Consumer}} and
C{L{Server}} use negotiators when
creating associations.
You can create and use negotiators if you:
- Do not want to do Diffie-Hellman key exchange because you use
transport-layer encryption (e.g. SSL)
- Want to use only SHA-256 associations
- Do not want to support plain-text associations over a non-secure
channel
It is up to you to set a policy for what kinds of associations to
accept. By default, the library will make any kind of association
that is allowed in the OpenID 2.0 specification.
Use of negotiators in the library
=================================
When a consumer makes an association request, it calls
C{L{getAllowedType}} to get the preferred association type and
association session type.
The server gets a request for a particular association/session
type and calls C{L{isAllowed}} to determine if it should
create an association. If it is supported, negotiation is
complete. If it is not, the server calls C{L{getAllowedType}} to
get an allowed association type to return to the consumer.
If the consumer gets an error response indicating that the
requested association/session type is not supported by the server
that contains an assocation/session type to try, it calls
C{L{isAllowed}} to determine if it should try again with the
given combination of association/session type.
@ivar allowed_types: A list of association/session types that are
allowed by the server. The order of the pairs in this list
determines preference. If an association/session type comes
earlier in the list, the library is more likely to use that
type.
@type allowed_types: [(str, str)]
"""
def __init__(self, allowed_types):
self.setAllowedTypes(allowed_types)
def copy(self):
return self.__class__(list(self.allowed_types))
def setAllowedTypes(self, allowed_types):
"""Set the allowed association types, checking to make sure
each combination is valid."""
for (assoc_type, session_type) in allowed_types:
checkSessionType(assoc_type, session_type)
self.allowed_types = allowed_types
def addAllowedType(self, assoc_type, session_type=None):
"""Add an association type and session type to the allowed
types list. The assocation/session pairs are tried in the
order that they are added."""
if self.allowed_types is None:
self.allowed_types = []
if session_type is None:
available = getSessionTypes(assoc_type)
if not available:
raise ValueError('No session available for association type %r'
% (assoc_type, ))
for session_type in getSessionTypes(assoc_type):
self.addAllowedType(assoc_type, session_type)
else:
checkSessionType(assoc_type, session_type)
self.allowed_types.append((assoc_type, session_type))
def isAllowed(self, assoc_type, session_type):
"""Is this combination of association type and session type allowed?"""
assoc_good = (assoc_type, session_type) in self.allowed_types
matches = session_type in getSessionTypes(assoc_type)
return assoc_good and matches
def getAllowedType(self):
"""Get a pair of assocation type and session type that are
supported"""
try:
return self.allowed_types[0]
except IndexError:
return (None, None)
default_negotiator = SessionNegotiator(default_association_order)
encrypted_negotiator = SessionNegotiator(only_encrypted_association_order)
def getSecretSize(assoc_type):
if assoc_type == 'HMAC-SHA1':
return 20
elif assoc_type == 'HMAC-SHA256':
return 32
else:
raise ValueError('Unsupported association type: %r' % (assoc_type, ))
@functools.total_ordering
class Association(object):
"""
This class represents an association between a server and a
consumer. In general, users of this library will never see
instances of this object. The only exception is if you implement
a custom C{L{OpenIDStore}}.
If you do implement such a store, it will need to store the values
of the C{L{handle}}, C{L{secret}}, C{L{issued}}, C{L{lifetime}}, and
C{L{assoc_type}} instance variables.
@ivar handle: This is the handle the server gave this association.
@type handle: C{str}
@ivar secret: This is the shared secret the server generated for
this association.
@type secret: C{str}
@ivar issued: This is the time this association was issued, in
seconds since 00:00 GMT, January 1, 1970. (ie, a unix
timestamp)
@type issued: C{int}
@ivar lifetime: This is the amount of time this association is
good for, measured in seconds since the association was
issued.
@type lifetime: C{int}
@ivar assoc_type: This is the type of association this instance
represents. The only valid value of this field at this time
is C{'HMAC-SHA1'}, but new types may be defined in the future.
@type assoc_type: C{str}
@sort: __init__, fromExpiresIn, expiresIn, __eq__, __ne__,
handle, secret, issued, lifetime, assoc_type
"""
# The ordering and name of keys as stored by serialize
assoc_keys = [
'version',
'handle',
'secret',
'issued',
'lifetime',
'assoc_type',
]
_macs = {
'HMAC-SHA1': cryptutil.hmacSha1,
'HMAC-SHA256': cryptutil.hmacSha256,
}
@classmethod
def fromExpiresIn(cls, expires_in, handle, secret, assoc_type):
"""
This is an alternate constructor used by the OpenID consumer
library to create associations. C{L{OpenIDStore
}} implementations
shouldn't use this constructor.
@param expires_in: This is the amount of time this association
is good for, measured in seconds since the association was
issued.
@type expires_in: C{int}
@param handle: This is the handle the server gave this
association.
@type handle: C{str}
@param secret: This is the shared secret the server generated
for this association.
@type secret: C{str}
@param assoc_type: This is the type of association this
instance represents. The only valid value of this field
at this time is C{'HMAC-SHA1'}, but new types may be
defined in the future.
@type assoc_type: C{str}
"""
issued = int(time.time())
lifetime = expires_in
return cls(handle, secret, issued, lifetime, assoc_type)
def __init__(self, handle, secret, issued, lifetime, assoc_type):
"""
This is the standard constructor for creating an association.
@param handle: This is the handle the server gave this
association.
@type handle: C{str}
@param secret: This is the shared secret the server generated
for this association.
@type secret: C{str}
@param issued: This is the time this association was issued,
in seconds since 00:00 GMT, January 1, 1970. (ie, a unix
timestamp)
@type issued: C{int}
@param lifetime: This is the amount of time this association
is good for, measured in seconds since the association was
issued.
@type lifetime: C{int}
@param assoc_type: This is the type of association this
instance represents. The only valid value of this field
at this time is C{'HMAC-SHA1'}, but new types may be
defined in the future.
@type assoc_type: C{str}
"""
if assoc_type not in all_association_types:
fmt = '%r is not a supported association type'
raise ValueError(fmt % (assoc_type, ))
# secret_size = getSecretSize(assoc_type)
# if len(secret) != secret_size:
# fmt = 'Wrong size secret (%s bytes) for association type %s'
# raise ValueError(fmt % (len(secret), assoc_type))
self.handle = handle
if isinstance(secret, str):
secret = secret.encode("utf-8") # should be bytes
self.secret = secret
self.issued = issued
self.lifetime = lifetime
self.assoc_type = assoc_type
@property
def expiresIn(self, now=None):
"""
This returns the number of seconds this association is still
valid for, or C{0} if the association is no longer valid.
@return: The number of seconds this association is still valid
for, or C{0} if the association is no longer valid.
@rtype: C{int}
"""
if now is None:
now = int(time.time())
return max(0, self.issued + self.lifetime - now)
def __lt__(self, other):
"""
Compare two C{L{Association}} instances to determine relative
ordering.
Currently compares object lifetimes -- C{L{Association}} A < B
if A.lifetime < B.lifetime.
"""
return self.lifetime < other.lifetime
def __eq__(self, other):
"""
This checks to see if two C{L{Association}} instances
represent the same association.
@return: C{True} if the two instances represent the same
association, C{False} otherwise.
@rtype: C{bool}
"""
return type(self) is type(other) and self.__dict__ == other.__dict__
def __ne__(self, other):
"""
This checks to see if two C{L{Association}} instances
represent different associations.
@return: C{True} if the two instances represent different
associations, C{False} otherwise.
@rtype: C{bool}
"""
return not (self == other)
def serialize(self):
"""
Convert an association to KV form.
@return: String in KV form suitable for deserialization by
deserialize.
@rtype: str
"""
data = {
'version': '2',
'handle': self.handle,
'secret': oidutil.toBase64(self.secret),
'issued': str(int(self.issued)),
'lifetime': str(int(self.lifetime)),
'assoc_type': self.assoc_type
}
assert len(data) == len(self.assoc_keys)
pairs = []
for field_name in self.assoc_keys:
pairs.append((field_name, data[field_name]))
return kvform.seqToKV(pairs, strict=True)
@classmethod
def deserialize(cls, assoc_s):
"""
Parse an association as stored by serialize().
inverse of serialize
@param assoc_s: Association as serialized by serialize()
@type assoc_s: bytes
@return: instance of this class
"""
pairs = kvform.kvToSeq(assoc_s, strict=True)
keys = []
values = []
for k, v in pairs:
keys.append(k)
values.append(v)
if keys != cls.assoc_keys:
raise ValueError('Unexpected key values: %r', keys)
version, handle, secret, issued, lifetime, assoc_type = values
if version != '2':
raise ValueError('Unknown version: %r' % version)
issued = int(issued)
lifetime = int(lifetime)
secret = oidutil.fromBase64(secret)
return cls(handle, secret, issued, lifetime, assoc_type)
def sign(self, pairs):
"""
Generate a signature for a sequence of (key, value) pairs
@param pairs: The pairs to sign, in order
@type pairs: sequence of (str, str)
@return: The binary signature of this sequence of pairs
@rtype: bytes
"""
kv = kvform.seqToKV(pairs)
try:
mac = self._macs[self.assoc_type]
except KeyError:
raise ValueError('Unknown association type: %r' %
(self.assoc_type, ))
return mac(self.secret, kv)
def getMessageSignature(self, message):
"""Return the signature of a message.
If I am not a sign-all association, the message must have a
signed list.
@return: the signature, base64 encoded
@rtype: bytes
@raises ValueError: If there is no signed list and I am not a sign-all
type of association.
"""
pairs = self._makePairs(message)
return oidutil.toBase64(self.sign(pairs))
def signMessage(self, message):
"""Add a signature (and a signed list) to a message.
@return: a new Message object with a signature
@rtype: L{openid.message.Message}
"""
if (message.hasKey(OPENID_NS, 'sig') or
message.hasKey(OPENID_NS, 'signed')):
raise ValueError('Message already has signed list or signature')
extant_handle = message.getArg(OPENID_NS, 'assoc_handle')
if extant_handle and extant_handle != self.handle:
raise ValueError("Message has a different association handle")
signed_message = message.copy()
signed_message.setArg(OPENID_NS, 'assoc_handle', self.handle)
message_keys = list(signed_message.toPostArgs().keys())
signed_list = [k[7:] for k in message_keys if k.startswith('openid.')]
signed_list.append('signed')
signed_list.sort()
signed_message.setArg(OPENID_NS, 'signed', ','.join(signed_list))
sig = self.getMessageSignature(signed_message)
signed_message.setArg(OPENID_NS, 'sig', sig)
return signed_message
def checkMessageSignature(self, message):
"""Given a message with a signature, calculate a new signature
and return whether it matches the signature in the message.
@raises ValueError: if the message has no signature or no signature
can be calculated for it.
"""
message_sig = message.getArg(OPENID_NS, 'sig')
if not message_sig:
raise ValueError("%s has no sig." % (message, ))
calculated_sig = self.getMessageSignature(message)
# remember, getMessageSignature returns bytes
calculated_sig = calculated_sig.decode('utf-8')
return cryptutil.const_eq(calculated_sig, message_sig)
def _makePairs(self, message):
signed = message.getArg(OPENID_NS, 'signed')
if not signed:
raise ValueError('Message has no signed list: %s' % (message, ))
signed_list = signed.split(',')
pairs = []
data = message.toPostArgs()
for field in signed_list:
pairs.append((field, data.get('openid.' + field, '')))
return pairs
def __repr__(self):
return "<%s.%s %s %s>" % (self.__class__.__module__,
self.__class__.__name__, self.assoc_type,
self.handle)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/codecutil.py 0000644 0001750 0001750 00000004350 00000000000 020104 0 ustar 00rami rami 0000000 0000000 import codecs
try:
chr(0x10000)
except ValueError:
# narrow python build
UCSCHAR = [
(0xA0, 0xD7FF),
(0xF900, 0xFDCF),
(0xFDF0, 0xFFEF),
]
IPRIVATE = [
(0xE000, 0xF8FF),
]
else:
UCSCHAR = [
(0xA0, 0xD7FF),
(0xF900, 0xFDCF),
(0xFDF0, 0xFFEF),
(0x10000, 0x1FFFD),
(0x20000, 0x2FFFD),
(0x30000, 0x3FFFD),
(0x40000, 0x4FFFD),
(0x50000, 0x5FFFD),
(0x60000, 0x6FFFD),
(0x70000, 0x7FFFD),
(0x80000, 0x8FFFD),
(0x90000, 0x9FFFD),
(0xA0000, 0xAFFFD),
(0xB0000, 0xBFFFD),
(0xC0000, 0xCFFFD),
(0xD0000, 0xDFFFD),
(0xE1000, 0xEFFFD),
]
IPRIVATE = [
(0xE000, 0xF8FF),
(0xF0000, 0xFFFFD),
(0x100000, 0x10FFFD),
]
_ESCAPE_RANGES = UCSCHAR + IPRIVATE
def _in_escape_range(octet):
for start, end in _ESCAPE_RANGES:
if start <= octet <= end:
return True
return False
def _starts_surrogate_pair(character):
char_value = ord(character)
return 0xD800 <= char_value <= 0xDBFF
def _ends_surrogate_pair(character):
char_value = ord(character)
return 0xDC00 <= char_value <= 0xDFFF
def _pct_encoded_replacements(chunk):
replacements = []
chunk_iter = iter(chunk)
for character in chunk_iter:
codepoint = ord(character)
if _in_escape_range(codepoint):
for char in chr(codepoint).encode("utf-8"):
replacements.append("%%%X" % char)
elif _starts_surrogate_pair(character):
next_character = next(chunk_iter)
for char in (character + next_character).encode("utf-8"):
replacements.append("%%%X" % char)
else:
replacements.append(chr(codepoint))
return replacements
def _pct_escape_handler(err):
'''
Encoding error handler that does percent-escaping of Unicode, to be used
with codecs.register_error
TODO: replace use of this with urllib.parse.quote as appropriate
'''
chunk = err.object[err.start:err.end]
replacements = _pct_encoded_replacements(chunk)
return ("".join(replacements), err.end)
codecs.register_error("oid_percent_escape", _pct_escape_handler)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8718696
python3-openid-3.2.0/openid/consumer/ 0000755 0001750 0001750 00000000000 00000000000 017410 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/consumer/__init__.py 0000644 0001750 0001750 00000000216 00000000000 021520 0 ustar 00rami rami 0000000 0000000 """
This package contains the portions of the library used only when
implementing an OpenID consumer.
"""
__all__ = ['consumer', 'discover']
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/openid/consumer/consumer.py 0000644 0001750 0001750 00000225736 00000000000 021634 0 ustar 00rami rami 0000000 0000000 # -*- test-case-name: openid.test.test_consumer -*-
"""OpenID support for Relying Parties (aka Consumers).
This module documents the main interface with the OpenID consumer
library. The only part of the library which has to be used and isn't
documented in full here is the store required to create an
C{L{Consumer}} instance. More on the abstract store type and
concrete implementations of it that are provided in the documentation
for the C{L{__init__}} method of the
C{L{Consumer}} class.
OVERVIEW
========
The OpenID identity verification process most commonly uses the
following steps, as visible to the user of this library:
1. The user enters their OpenID into a field on the consumer's
site, and hits a login button.
2. The consumer site discovers the user's OpenID provider using
the Yadis protocol.
3. The consumer site sends the browser a redirect to the
OpenID provider. This is the authentication request as
described in the OpenID specification.
4. The OpenID provider's site sends the browser a redirect
back to the consumer site. This redirect contains the
provider's response to the authentication request.
The most important part of the flow to note is the consumer's site
must handle two separate HTTP requests in order to perform the
full identity check.
LIBRARY DESIGN
==============
This consumer library is designed with that flow in mind. The
goal is to make it as easy as possible to perform the above steps
securely.
At a high level, there are two important parts in the consumer
library. The first important part is this module, which contains
the interface to actually use this library. The second is the
C{L{openid.store.interface}} module, which describes the
interface to use if you need to create a custom method for storing
the state this library needs to maintain between requests.
In general, the second part is less important for users of the
library to know about, as several implementations are provided
which cover a wide variety of situations in which consumers may
use the library.
This module contains a class, C{L{Consumer}}, with methods
corresponding to the actions necessary in each of steps 2, 3, and
4 described in the overview. Use of this library should be as easy
as creating an C{L{Consumer}} instance and calling the methods
appropriate for the action the site wants to take.
SESSIONS, STORES, AND STATELESS MODE
====================================
The C{L{Consumer}} object keeps track of two types of state:
1. State of the user's current authentication attempt. Things like
the identity URL, the list of endpoints discovered for that
URL, and in case where some endpoints are unreachable, the list
of endpoints already tried. This state needs to be held from
Consumer.begin() to Consumer.complete(), but it is only applicable
to a single session with a single user agent, and at the end of
the authentication process (i.e. when an OP replies with either
C{id_res} or C{cancel}) it may be discarded.
2. State of relationships with servers, i.e. shared secrets
(associations) with servers and nonces seen on signed messages.
This information should persist from one session to the next and
should not be bound to a particular user-agent.
These two types of storage are reflected in the first two arguments of
Consumer's constructor, C{session} and C{store}. C{session} is a
dict-like object and we hope your web framework provides you with one
of these bound to the user agent. C{store} is an instance of
L{openid.store.interface.OpenIDStore}.
Since the store does hold secrets shared between your application and the
OpenID provider, you should be careful about how you use it in a shared
hosting environment. If the filesystem or database permissions of your
web host allow strangers to read from them, do not store your data there!
If you have no safe place to store your data, construct your consumer
with C{None} for the store, and it will operate only in stateless mode.
Stateless mode may be slower, put more load on the OpenID provider, and
trusts the provider to keep you safe from replay attacks.
Several store implementation are provided, and the interface is
fully documented so that custom stores can be used as well. See
the documentation for the C{L{Consumer}} class for more
information on the interface for stores. The implementations that
are provided allow the consumer site to store the necessary data
in several different ways, including several SQL databases and
normal files on disk.
IMMEDIATE MODE
==============
In the flow described above, the user may need to confirm to the
OpenID provider that it's ok to disclose his or her identity.
The provider may draw pages asking for information from the user
before it redirects the browser back to the consumer's site. This
is generally transparent to the consumer site, so it is typically
ignored as an implementation detail.
There can be times, however, where the consumer site wants to get
a response immediately. When this is the case, the consumer can
put the library in immediate mode. In immediate mode, there is an
extra response possible from the server, which is essentially the
server reporting that it doesn't have enough information to answer
the question yet.
USING THIS LIBRARY
==================
Integrating this library into an application is usually a
relatively straightforward process. The process should basically
follow this plan:
Add an OpenID login field somewhere on your site. When an OpenID
is entered in that field and the form is submitted, it should make
a request to your site which includes that OpenID URL.
First, the application should L{instantiate a Consumer}
with a session for per-user state and store for shared state.
using the store of choice.
Next, the application should call the 'C{L{begin}}' method on the
C{L{Consumer}} instance. This method takes the OpenID URL. The
C{L{begin}} method returns an C{L{AuthRequest}}
object.
Next, the application should call the
C{L{redirectURL}} method on the
C{L{AuthRequest}} object. The parameter C{return_to} is the URL
that the OpenID server will send the user back to after attempting
to verify his or her identity. The C{realm} parameter is the
URL (or URL pattern) that identifies your web site to the user
when he or she is authorizing it. Send a redirect to the
resulting URL to the user's browser.
That's the first half of the authentication process. The second
half of the process is done after the user's OpenID Provider sends the
user's browser a redirect back to your site to complete their
login.
When that happens, the user will contact your site at the URL
given as the C{return_to} URL to the
C{L{redirectURL}} call made
above. The request will have several query parameters added to
the URL by the OpenID provider as the information necessary to
finish the request.
Get a C{L{Consumer}} instance with the same session and store as
before and call its C{L{complete}} method,
passing in all the received query arguments.
There are multiple possible return types possible from that
method. These indicate whether or not the login was successful,
and include any additional information appropriate for their type.
@var SUCCESS: constant used as the status for
L{SuccessResponse} objects.
@var FAILURE: constant used as the status for
L{FailureResponse} objects.
@var CANCEL: constant used as the status for
L{CancelResponse} objects.
@var SETUP_NEEDED: constant used as the status for
L{SetupNeededResponse}
objects.
"""
import copy
import logging
from urllib.parse import urlparse, urldefrag, parse_qsl
from openid import fetchers
from openid.consumer.discover import discover, OpenIDServiceEndpoint, \
DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE
from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \
IDENTIFIER_SELECT, no_default, BARE_NS
from openid import cryptutil
from openid import oidutil
from openid.association import Association, default_negotiator, \
SessionNegotiator
from openid.dh import DiffieHellman
from openid.store.nonce import mkNonce, split as splitNonce
from openid.yadis.manager import Discovery
from openid import urinorm
__all__ = [
'AuthRequest',
'Consumer',
'SuccessResponse',
'SetupNeededResponse',
'CancelResponse',
'FailureResponse',
'SUCCESS',
'FAILURE',
'CANCEL',
'SETUP_NEEDED',
]
logger = logging.getLogger(__name__)
def makeKVPost(request_message, server_url):
"""Make a Direct Request to an OpenID Provider and return the
result as a Message object.
@raises openid.fetchers.HTTPFetchingError: if an error is
encountered in making the HTTP post.
@rtype: L{openid.message.Message}
"""
# XXX: TESTME
resp = fetchers.fetch(server_url, body=request_message.toURLEncoded())
# Process response in separate function that can be shared by async code.
return _httpResponseToMessage(resp, server_url)
def _httpResponseToMessage(response, server_url):
"""Adapt a POST response to a Message.
@type response: L{openid.fetchers.HTTPResponse}
@param response: Result of a POST to an OpenID endpoint.
@rtype: L{openid.message.Message}
@raises openid.fetchers.HTTPFetchingError: if the server returned a
status of other than 200 or 400.
@raises ServerError: if the server returned an OpenID error.
"""
# Should this function be named Message.fromHTTPResponse instead?
response_message = Message.fromKVForm(response.body)
if response.status == 400:
raise ServerError.fromMessage(response_message)
elif response.status not in (200, 206):
fmt = 'bad status code from server %s: %s'
error_message = fmt % (server_url, response.status)
raise fetchers.HTTPFetchingError(error_message)
return response_message
class Consumer(object):
"""An OpenID consumer implementation that performs discovery and
does session management.
@ivar consumer: an instance of an object implementing the OpenID
protocol, but doing no discovery or session management.
@type consumer: GenericConsumer
@ivar session: A dictionary-like object representing the user's
session data. This is used for keeping state of the OpenID
transaction when the user is redirected to the server.
@cvar session_key_prefix: A string that is prepended to session
keys to ensure that they are unique. This variable may be
changed to suit your application.
"""
session_key_prefix = "_openid_consumer_"
_token = 'last_token'
_discover = staticmethod(discover)
def __init__(self, session, store, consumer_class=None):
"""Initialize a Consumer instance.
You should create a new instance of the Consumer object with
every HTTP request that handles OpenID transactions.
@param session: See L{the session instance variable}
@param store: an object that implements the interface in
C{L{openid.store.interface.OpenIDStore}}. Several
implementations are provided, to cover common database
environments.
@type store: C{L{openid.store.interface.OpenIDStore}}
@see: L{openid.store.interface}
@see: L{openid.store}
"""
self.session = session
if consumer_class is None:
consumer_class = GenericConsumer
self.consumer = consumer_class(store)
self._token_key = self.session_key_prefix + self._token
def begin(self, user_url, anonymous=False):
"""Start the OpenID authentication process. See steps 1-2 in
the overview at the top of this file.
@param user_url: Identity URL given by the user. This method
performs a textual transformation of the URL to try and
make sure it is normalized. For example, a user_url of
example.com will be normalized to http://example.com/
normalizing and resolving any redirects the server might
issue.
@type user_url: unicode
@param anonymous: Whether to make an anonymous request of the OpenID
provider. Such a request does not ask for an authorization
assertion for an OpenID identifier, but may be used with
extensions to pass other data. e.g. "I don't care who you are,
but I'd like to know your time zone."
@type anonymous: bool
@returns: An object containing the discovered information will
be returned, with a method for building a redirect URL to
the server, as described in step 3 of the overview. This
object may also be used to add extension arguments to the
request, using its
L{addExtensionArg}
method.
@returntype: L{AuthRequest}
@raises openid.consumer.discover.DiscoveryFailure: when I fail to
find an OpenID server for this URL. If the C{yadis} package
is available, L{openid.consumer.discover.DiscoveryFailure} is
an alias for C{yadis.discover.DiscoveryFailure}.
"""
disco = Discovery(self.session, user_url, self.session_key_prefix)
try:
service = disco.getNextService(self._discover)
except fetchers.HTTPFetchingError as why:
raise DiscoveryFailure('Error fetching XRDS document: %s' %
(why.why, ), None)
if service is None:
raise DiscoveryFailure('No usable OpenID services found for %s' %
(user_url, ), None)
else:
return self.beginWithoutDiscovery(service, anonymous)
def beginWithoutDiscovery(self, service, anonymous=False):
"""Start OpenID verification without doing OpenID server
discovery. This method is used internally by Consumer.begin
after discovery is performed, and exists to provide an
interface for library users needing to perform their own
discovery.
@param service: an OpenID service endpoint descriptor. This
object and factories for it are found in the
L{openid.consumer.discover} module.
@type service:
L{OpenIDServiceEndpoint}
@returns: an OpenID authentication request object.
@rtype: L{AuthRequest}
@See: Openid.consumer.consumer.Consumer.begin
@see: openid.consumer.discover
"""
auth_req = self.consumer.begin(service)
self.session[self._token_key] = auth_req.endpoint
try:
auth_req.setAnonymous(anonymous)
except ValueError as why:
raise ProtocolError(str(why))
return auth_req
def complete(self, query, current_url):
"""Called to interpret the server's response to an OpenID
request. It is called in step 4 of the flow described in the
consumer overview.
@param query: A dictionary of the query parameters for this
HTTP request.
@param current_url: The URL used to invoke the application.
Extract the URL from your application's web
request framework and specify it here to have it checked
against the openid.return_to value in the response. If
the return_to URL check fails, the status of the
completion will be FAILURE.
@returns: a subclass of Response. The type of response is
indicated by the status attribute, which will be one of
SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
@see: L{SuccessResponse}
@see: L{CancelResponse}
@see: L{SetupNeededResponse}
@see: L{FailureResponse}
"""
endpoint = self.session.get(self._token_key)
message = Message.fromPostArgs(query)
response = self.consumer.complete(message, endpoint, current_url)
try:
del self.session[self._token_key]
except KeyError:
pass
if (response.status in ['success', 'cancel'] and
response.identity_url is not None):
disco = Discovery(self.session, response.identity_url,
self.session_key_prefix)
# This is OK to do even if we did not do discovery in
# the first place.
disco.cleanup(force=True)
return response
def setAssociationPreference(self, association_preferences):
"""Set the order in which association types/sessions should be
attempted. For instance, to only allow HMAC-SHA256
associations created with a DH-SHA256 association session:
>>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')])
Any association type/association type pair that is not in this
list will not be attempted at all.
@param association_preferences: The list of allowed
(association type, association session type) pairs that
should be allowed for this consumer to use, in order from
most preferred to least preferred.
@type association_preferences: [(str, str)]
@returns: None
@see: C{L{openid.association.SessionNegotiator}}
"""
self.consumer.negotiator = SessionNegotiator(association_preferences)
class DiffieHellmanSHA1ConsumerSession(object):
session_type = 'DH-SHA1'
hash_func = staticmethod(cryptutil.sha1)
secret_size = 20
allowed_assoc_types = ['HMAC-SHA1']
def __init__(self, dh=None):
if dh is None:
dh = DiffieHellman.fromDefaults()
self.dh = dh
def getRequest(self):
cpub = cryptutil.longToBase64(self.dh.public)
args = {'dh_consumer_public': cpub}
if not self.dh.usingDefaultValues():
args.update({
'dh_modulus': cryptutil.longToBase64(self.dh.modulus),
'dh_gen': cryptutil.longToBase64(self.dh.generator),
})
return args
def extractSecret(self, response):
dh_server_public64 = response.getArg(OPENID_NS, 'dh_server_public',
no_default)
enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default)
dh_server_public = cryptutil.base64ToLong(dh_server_public64)
enc_mac_key = oidutil.fromBase64(enc_mac_key64)
return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func)
class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession):
session_type = 'DH-SHA256'
hash_func = staticmethod(cryptutil.sha256)
secret_size = 32
allowed_assoc_types = ['HMAC-SHA256']
class PlainTextConsumerSession(object):
session_type = 'no-encryption'
allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
def getRequest(self):
return {}
def extractSecret(self, response):
mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default)
return oidutil.fromBase64(mac_key64)
class SetupNeededError(Exception):
"""Internally-used exception that indicates that an immediate-mode
request cancelled."""
def __init__(self, user_setup_url=None):
Exception.__init__(self, user_setup_url)
self.user_setup_url = user_setup_url
class ProtocolError(ValueError):
"""Exception that indicates that a message violated the
protocol. It is raised and caught internally to this file."""
class TypeURIMismatch(ProtocolError):
"""A protocol error arising from type URIs mismatching
"""
def __init__(self, expected, endpoint):
ProtocolError.__init__(self, expected, endpoint)
self.expected = expected
self.endpoint = endpoint
def __str__(self):
s = '<%s.%s: Required type %s not found in %s for endpoint %s>' % (
self.__class__.__module__, self.__class__.__name__, self.expected,
self.endpoint.type_uris, self.endpoint)
return s
class ServerError(Exception):
"""Exception that is raised when the server returns a 400 response
code to a direct request."""
def __init__(self, error_text, error_code, message):
Exception.__init__(self, error_text)
self.error_text = error_text
self.error_code = error_code
self.message = message
def fromMessage(cls, message):
"""Generate a ServerError instance, extracting the error text
and the error code from the message."""
error_text = message.getArg(OPENID_NS, 'error',
'')
error_code = message.getArg(OPENID_NS, 'error_code')
return cls(error_text, error_code, message)
fromMessage = classmethod(fromMessage)
class GenericConsumer(object):
"""This is the implementation of the common logic for OpenID
consumers. It is unaware of the application in which it is
running.
@ivar negotiator: An object that controls the kind of associations
that the consumer makes. It defaults to
C{L{openid.association.default_negotiator}}. Assign a
different negotiator to it if you have specific requirements
for how associations are made.
@type negotiator: C{L{openid.association.SessionNegotiator}}
"""
# The name of the query parameter that gets added to the return_to
# URL when using OpenID1. You can change this value if you want or
# need a different name, but don't make it start with openid,
# because it's not a standard protocol thing for OpenID1. For
# OpenID2, the library will take care of the nonce using standard
# OpenID query parameter names.
openid1_nonce_query_arg_name = 'janrain_nonce'
# Another query parameter that gets added to the return_to for
# OpenID 1; if the user's session state is lost, use this claimed
# identifier to do discovery when verifying the response.
openid1_return_to_identifier_name = 'openid1_claimed_id'
session_types = {
'DH-SHA1': DiffieHellmanSHA1ConsumerSession,
'DH-SHA256': DiffieHellmanSHA256ConsumerSession,
'no-encryption': PlainTextConsumerSession,
}
_discover = staticmethod(discover)
def __init__(self, store):
self.store = store
self.negotiator = default_negotiator.copy()
def begin(self, service_endpoint):
"""Create an AuthRequest object for the specified
service_endpoint. This method will create an association if
necessary."""
if self.store is None:
assoc = None
else:
assoc = self._getAssociation(service_endpoint)
request = AuthRequest(service_endpoint, assoc)
request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce()
if request.message.isOpenID1():
request.return_to_args[self.openid1_return_to_identifier_name] = \
request.endpoint.claimed_id
return request
def complete(self, message, endpoint, return_to):
"""Process the OpenID message, using the specified endpoint
and return_to URL as context. This method will handle any
OpenID message that is sent to the return_to URL.
"""
mode = message.getArg(OPENID_NS, 'mode', '')
modeMethod = getattr(self, '_complete_' + mode, self._completeInvalid)
return modeMethod(message, endpoint, return_to)
def _complete_cancel(self, message, endpoint, _):
return CancelResponse(endpoint)
def _complete_error(self, message, endpoint, _):
error = message.getArg(OPENID_NS, 'error')
contact = message.getArg(OPENID_NS, 'contact')
reference = message.getArg(OPENID_NS, 'reference')
return FailureResponse(
endpoint, error, contact=contact, reference=reference)
def _complete_setup_needed(self, message, endpoint, _):
if not message.isOpenID2():
return self._completeInvalid(message, endpoint, _)
user_setup_url = message.getArg(OPENID2_NS, 'user_setup_url')
return SetupNeededResponse(endpoint, user_setup_url)
def _complete_id_res(self, message, endpoint, return_to):
try:
self._checkSetupNeeded(message)
except SetupNeededError as why:
return SetupNeededResponse(endpoint, why.user_setup_url)
else:
try:
return self._doIdRes(message, endpoint, return_to)
except (ProtocolError, DiscoveryFailure) as why:
return FailureResponse(endpoint, why)
def _completeInvalid(self, message, endpoint, _):
mode = message.getArg(OPENID_NS, 'mode', '')
return FailureResponse(endpoint, 'Invalid openid.mode: %r' % (mode, ))
def _checkReturnTo(self, message, return_to):
"""Check an OpenID message and its openid.return_to value
against a return_to URL from an application. Return True on
success, False on failure.
"""
# Check the openid.return_to args against args in the original
# message.
try:
self._verifyReturnToArgs(message.toPostArgs())
except ProtocolError as why:
logger.exception("Verifying return_to arguments: %s" % (why, ))
return False
# Check the return_to base URL against the one in the message.
msg_return_to = message.getArg(OPENID_NS, 'return_to')
# The URL scheme, authority, and path MUST be the same between
# the two URLs.
app_parts = urlparse(urinorm.urinorm(return_to))
msg_parts = urlparse(urinorm.urinorm(msg_return_to))
# (addressing scheme, network location, path) must be equal in
# both URLs.
for part in range(0, 3):
if app_parts[part] != msg_parts[part]:
return False
return True
_makeKVPost = staticmethod(makeKVPost)
def _checkSetupNeeded(self, message):
"""Check an id_res message to see if it is a
checkid_immediate cancel response.
@raises SetupNeededError: if it is a checkid_immediate cancellation
"""
# In OpenID 1, we check to see if this is a cancel from
# immediate mode by the presence of the user_setup_url
# parameter.
if message.isOpenID1():
user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url')
if user_setup_url is not None:
raise SetupNeededError(user_setup_url)
def _doIdRes(self, message, endpoint, return_to):
"""Handle id_res responses that are not cancellations of
immediate mode requests.
@param message: the response paramaters.
@param endpoint: the discovered endpoint object. May be None.
@raises ProtocolError: If the message contents are not
well-formed according to the OpenID specification. This
includes missing fields or not signing fields that should
be signed.
@raises DiscoveryFailure: If the subject of the id_res message
does not match the supplied endpoint, and discovery on the
identifier in the message fails (this should only happen
when using OpenID 2)
@returntype: L{Response}
"""
# Checks for presence of appropriate fields (and checks
# signed list fields)
self._idResCheckForFields(message)
if not self._checkReturnTo(message, return_to):
raise ProtocolError(
"return_to does not match return URL. Expected %r, got %r" %
(return_to, message.getArg(OPENID_NS, 'return_to')))
# Verify discovery information:
endpoint = self._verifyDiscoveryResults(message, endpoint)
logger.info("Received id_res response from %s using association %s" %
(endpoint.server_url,
message.getArg(OPENID_NS, 'assoc_handle')))
self._idResCheckSignature(message, endpoint.server_url)
# Will raise a ProtocolError if the nonce is bad
self._idResCheckNonce(message, endpoint)
signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
signed_list = signed_list_str.split(',')
signed_fields = ["openid." + s for s in signed_list]
return SuccessResponse(endpoint, message, signed_fields)
def _idResGetNonceOpenID1(self, message, endpoint):
"""Extract the nonce from an OpenID 1 response. Return the
nonce from the BARE_NS since we independently check the
return_to arguments are the same as those in the response
message.
See the openid1_nonce_query_arg_name class variable
@returns: The nonce as a string or None
"""
return message.getArg(BARE_NS, self.openid1_nonce_query_arg_name)
def _idResCheckNonce(self, message, endpoint):
if message.isOpenID1():
# This indicates that the nonce was generated by the consumer
nonce = self._idResGetNonceOpenID1(message, endpoint)
server_url = ''
else:
nonce = message.getArg(OPENID2_NS, 'response_nonce')
server_url = endpoint.server_url
if nonce is None:
raise ProtocolError('Nonce missing from response')
try:
timestamp, salt = splitNonce(nonce)
except ValueError as why:
raise ProtocolError('Malformed nonce: %s' % (why, ))
if (self.store is not None and
not self.store.useNonce(server_url, timestamp, salt)):
raise ProtocolError('Nonce already used or out of range')
def _idResCheckSignature(self, message, server_url):
assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
if self.store is None:
assoc = None
else:
assoc = self.store.getAssociation(server_url, assoc_handle)
if assoc:
if assoc.expiresIn <= 0:
# XXX: It might be a good idea sometimes to re-start the
# authentication with a new association. Doing it
# automatically opens the possibility for
# denial-of-service by a server that just returns expired
# associations (or really short-lived associations)
raise ProtocolError('Association with %s expired' %
(server_url, ))
if not assoc.checkMessageSignature(message):
raise ProtocolError('Bad signature')
else:
# It's not an association we know about. Stateless mode is our
# only possible path for recovery.
# XXX - async framework will not want to block on this call to
# _checkAuth.
if not self._checkAuth(message, server_url):
raise ProtocolError('Server denied check_authentication')
def _idResCheckForFields(self, message):
# XXX: this should be handled by the code that processes the
# response (that is, if a field is missing, we should not have
# to explicitly check that it's present, just make sure that
# the fields are actually being used by the rest of the code
# in tests). Although, which fields are signed does need to be
# checked somewhere.
basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
basic_sig_fields = ['return_to', 'identity']
require_fields = {
OPENID2_NS: basic_fields + ['op_endpoint'],
OPENID1_NS: basic_fields + ['identity'],
}
require_sigs = {
OPENID2_NS:
basic_sig_fields +
['response_nonce', 'claimed_id', 'assoc_handle', 'op_endpoint'],
OPENID1_NS:
basic_sig_fields,
}
for field in require_fields[message.getOpenIDNamespace()]:
if not message.hasKey(OPENID_NS, field):
raise ProtocolError('Missing required field %r' % (field, ))
signed_list_str = message.getArg(OPENID_NS, 'signed', no_default)
signed_list = signed_list_str.split(',')
for field in require_sigs[message.getOpenIDNamespace()]:
# Field is present and not in signed list
if message.hasKey(OPENID_NS, field) and field not in signed_list:
raise ProtocolError('"%s" not signed' % (field, ))
def _verifyReturnToArgs(query):
"""Verify that the arguments in the return_to URL are present in this
response.
"""
# NOTE -- query came from Message.toPostArgs, which returns a dict of
# {str: str}
message = Message.fromPostArgs(query)
return_to = message.getArg(OPENID_NS, 'return_to')
if return_to is None:
raise ProtocolError('Response has no return_to')
parsed_url = urlparse(return_to)
rt_query = parsed_url[4]
parsed_args = parse_qsl(rt_query)
# NOTE -- parsed_args will be a dict of {bytes: bytes}, however it
# will be checked against return values from Message methods which are
# {str: str}. We need to compare apples to apples.
for rt_key, rt_value in parsed_args:
try:
value = query[rt_key]
if rt_value != value:
format = ("parameter %s value %r does not match "
"return_to's value %r")
raise ProtocolError(format % (rt_key, value, rt_value))
except KeyError:
format = "return_to parameter %s absent from query %r"
raise ProtocolError(format % (rt_key, query))
# Make sure all non-OpenID arguments in the response are also
# in the signed return_to.
bare_args = message.getArgs(BARE_NS)
for pair in bare_args.items():
if pair not in parsed_args:
raise ProtocolError("Parameter %s not in return_to URL" %
(pair[0], ))
_verifyReturnToArgs = staticmethod(_verifyReturnToArgs)
def _verifyDiscoveryResults(self, resp_msg, endpoint=None):
"""
Extract the information from an OpenID assertion message and
verify it against the original
@param endpoint: The endpoint that resulted from doing discovery
@param resp_msg: The id_res message object
@returns: the verified endpoint
"""
if resp_msg.getOpenIDNamespace() == OPENID2_NS:
return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint)
else:
return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint)
def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint):
to_match = OpenIDServiceEndpoint()
to_match.type_uris = [OPENID_2_0_TYPE]
to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id')
to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity')
# Raises a KeyError when the op_endpoint is not present
to_match.server_url = resp_msg.getArg(OPENID2_NS, 'op_endpoint',
no_default)
# claimed_id and identifier must both be present or both
# be absent
if (to_match.claimed_id is None and to_match.local_id is not None):
raise ProtocolError(
'openid.identity is present without openid.claimed_id')
elif (to_match.claimed_id is not None and to_match.local_id is None):
raise ProtocolError(
'openid.claimed_id is present without openid.identity')
# This is a response without identifiers, so there's really no
# checking that we can do, so return an endpoint that's for
# the specified `openid.op_endpoint'
elif to_match.claimed_id is None:
return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url)
# The claimed ID doesn't match, so we have to do discovery
# again. This covers not using sessions, OP identifier
# endpoints and responses that didn't match the original
# request.
if not endpoint:
logger.info('No pre-discovered information supplied.')
endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match])
elif endpoint.isOPIdentifier():
logger.info(
'Pre-discovered information based on OP-ID; need to rediscover.'
)
endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match])
else:
# The claimed ID matches, so we use the endpoint that we
# discovered in initiation. This should be the most common
# case.
try:
self._verifyDiscoverySingle(endpoint, to_match)
except ProtocolError as e:
logger.exception(
"Error attempting to use stored discovery information: " +
str(e))
logger.info("Attempting discovery to verify endpoint")
endpoint = self._discoverAndVerify(to_match.claimed_id,
[to_match])
# The endpoint we return should have the claimed ID from the
# message we just verified, fragment and all.
if endpoint.claimed_id != to_match.claimed_id:
endpoint = copy.copy(endpoint)
endpoint.claimed_id = to_match.claimed_id
return endpoint
def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint):
claimed_id = resp_msg.getArg(BARE_NS,
self.openid1_return_to_identifier_name)
if endpoint is None and claimed_id is None:
raise RuntimeError(
'When using OpenID 1, the claimed ID must be supplied, '
'either by passing it through as a return_to parameter '
'or by using a session, and supplied to the GenericConsumer '
'as the argument to complete()')
elif endpoint is not None and claimed_id is None:
claimed_id = endpoint.claimed_id
to_match = OpenIDServiceEndpoint()
to_match.type_uris = [OPENID_1_1_TYPE]
to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity')
# Restore delegate information from the initiation phase
to_match.claimed_id = claimed_id
if to_match.local_id is None:
raise ProtocolError('Missing required field openid.identity')
to_match_1_0 = copy.copy(to_match)
to_match_1_0.type_uris = [OPENID_1_0_TYPE]
if endpoint is not None:
try:
try:
self._verifyDiscoverySingle(endpoint, to_match)
except TypeURIMismatch:
self._verifyDiscoverySingle(endpoint, to_match_1_0)
except ProtocolError as e:
logger.exception(
"Error attempting to use stored discovery information: " +
str(e))
logger.info("Attempting discovery to verify endpoint")
else:
return endpoint
# Endpoint is either bad (failed verification) or None
return self._discoverAndVerify(claimed_id, [to_match, to_match_1_0])
def _verifyDiscoverySingle(self, endpoint, to_match):
"""Verify that the given endpoint matches the information
extracted from the OpenID assertion, and raise an exception if
there is a mismatch.
@type endpoint: openid.consumer.discover.OpenIDServiceEndpoint
@type to_match: openid.consumer.discover.OpenIDServiceEndpoint
@rtype: NoneType
@raises ProtocolError: when the endpoint does not match the
discovered information.
"""
# Every type URI that's in the to_match endpoint has to be
# present in the discovered endpoint.
for type_uri in to_match.type_uris:
if not endpoint.usesExtension(type_uri):
raise TypeURIMismatch(type_uri, endpoint)
# Fragments do not influence discovery, so we can't compare a
# claimed identifier with a fragment to discovered information.
defragged_claimed_id, _ = urldefrag(to_match.claimed_id)
if defragged_claimed_id != endpoint.claimed_id:
raise ProtocolError(
'Claimed ID does not match (different subjects!), '
'Expected %s, got %s' %
(defragged_claimed_id, endpoint.claimed_id))
if to_match.getLocalID() != endpoint.getLocalID():
raise ProtocolError('local_id mismatch. Expected %s, got %s' %
(to_match.getLocalID(), endpoint.getLocalID()))
# If the server URL is None, this must be an OpenID 1
# response, because op_endpoint is a required parameter in
# OpenID 2. In that case, we don't actually care what the
# discovered server_url is, because signature checking or
# check_auth should take care of that check for us.
if to_match.server_url is None:
assert to_match.preferredNamespace() == OPENID1_NS, (
"""The code calling this must ensure that OpenID 2
responses have a non-none `openid.op_endpoint' and
that it is set as the `server_url' attribute of the
`to_match' endpoint.""")
elif to_match.server_url != endpoint.server_url:
raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' %
(to_match.server_url, endpoint.server_url))
def _discoverAndVerify(self, claimed_id, to_match_endpoints):
"""Given an endpoint object created from the information in an
OpenID response, perform discovery and verify the discovery
results, returning the matching endpoint that is the result of
doing that discovery.
@type to_match: openid.consumer.discover.OpenIDServiceEndpoint
@param to_match: The endpoint whose information we're confirming
@rtype: openid.consumer.discover.OpenIDServiceEndpoint
@returns: The result of performing discovery on the claimed
identifier in `to_match'
@raises DiscoveryFailure: when discovery fails.
"""
logger.info('Performing discovery on %s' % (claimed_id, ))
_, services = self._discover(claimed_id)
if not services:
raise DiscoveryFailure('No OpenID information found at %s' %
(claimed_id, ), None)
return self._verifyDiscoveredServices(claimed_id, services,
to_match_endpoints)
def _verifyDiscoveredServices(self, claimed_id, services,
to_match_endpoints):
"""See @L{_discoverAndVerify}"""
# Search the services resulting from discovery to find one
# that matches the information from the assertion
failure_messages = []
for endpoint in services:
for to_match_endpoint in to_match_endpoints:
try:
self._verifyDiscoverySingle(endpoint, to_match_endpoint)
except ProtocolError as why:
failure_messages.append(str(why))
else:
# It matches, so discover verification has
# succeeded. Return this endpoint.
return endpoint
else:
logger.error('Discovery verification failure for %s' %
(claimed_id, ))
for failure_message in failure_messages:
logger.error(' * Endpoint mismatch: ' + failure_message)
raise DiscoveryFailure(
'No matching endpoint found after discovering %s' %
(claimed_id, ), None)
def _checkAuth(self, message, server_url):
"""Make a check_authentication request to verify this message.
@returns: True if the request is valid.
@rtype: bool
"""
logger.info('Using OpenID check_authentication')
request = self._createCheckAuthRequest(message)
if request is None:
return False
try:
response = self._makeKVPost(request, server_url)
except (fetchers.HTTPFetchingError, ServerError) as e:
e0 = e.args[0]
logger.exception('check_authentication failed: %s' % e0)
return False
else:
return self._processCheckAuthResponse(response, server_url)
def _createCheckAuthRequest(self, message):
"""Generate a check_authentication request message given an
id_res message.
"""
signed = message.getArg(OPENID_NS, 'signed')
if signed:
if isinstance(signed, bytes):
signed = str(signed, encoding="utf-8")
for k in signed.split(','):
logger.info(k)
val = message.getAliasedArg(k)
# Signed value is missing
if val is None:
logger.info('Missing signed field %r' % (k, ))
return None
check_auth_message = message.copy()
check_auth_message.setArg(OPENID_NS, 'mode', 'check_authentication')
return check_auth_message
def _processCheckAuthResponse(self, response, server_url):
"""Process the response message from a check_authentication
request, invalidating associations if requested.
"""
is_valid = response.getArg(OPENID_NS, 'is_valid', 'false')
invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle')
if invalidate_handle is not None:
logger.info('Received "invalidate_handle" from server %s' %
(server_url, ))
if self.store is None:
logger.error('Unexpectedly got invalidate_handle without '
'a store!')
else:
self.store.removeAssociation(server_url, invalidate_handle)
if is_valid == 'true':
return True
else:
logger.error('Server responds that checkAuth call is not valid')
return False
def _getAssociation(self, endpoint):
"""Get an association for the endpoint's server_url.
First try seeing if we have a good association in the
store. If we do not, then attempt to negotiate an association
with the server.
If we negotiate a good association, it will get stored.
@returns: A valid association for the endpoint's server_url or None
@rtype: openid.association.Association or NoneType
"""
assoc = self.store.getAssociation(endpoint.server_url)
if assoc is None or assoc.expiresIn <= 0:
assoc = self._negotiateAssociation(endpoint)
if assoc is not None:
self.store.storeAssociation(endpoint.server_url, assoc)
return assoc
def _negotiateAssociation(self, endpoint):
"""Make association requests to the server, attempting to
create a new association.
@returns: a new association object
@rtype: L{openid.association.Association}
"""
# Get our preferred session/association type from the negotiatior.
assoc_type, session_type = self.negotiator.getAllowedType()
try:
assoc = self._requestAssociation(endpoint, assoc_type,
session_type)
except ServerError as why:
supportedTypes = self._extractSupportedAssociationType(
why, endpoint, assoc_type)
if supportedTypes is not None:
assoc_type, session_type = supportedTypes
# Attempt to create an association from the assoc_type
# and session_type that the server told us it
# supported.
try:
assoc = self._requestAssociation(endpoint, assoc_type,
session_type)
except ServerError as why:
# Do not keep trying, since it rejected the
# association type that it told us to use.
logger.error(
'Server %s refused its suggested association '
'type: session_type=%s, assoc_type=%s' % (
endpoint.server_url, session_type, assoc_type))
return None
else:
return assoc
else:
return assoc
def _extractSupportedAssociationType(self, server_error, endpoint,
assoc_type):
"""Handle ServerErrors resulting from association requests.
@returns: If server replied with an C{unsupported-type} error,
return a tuple of supported C{association_type}, C{session_type}.
Otherwise logs the error and returns None.
@rtype: tuple or None
"""
# Any error message whose code is not 'unsupported-type'
# should be considered a total failure.
if server_error.error_code != 'unsupported-type' or \
server_error.message.isOpenID1():
logger.error(
'Server error when requesting an association from %r: %s' %
(endpoint.server_url, server_error.error_text))
return None
# The server didn't like the association/session type
# that we sent, and it sent us back a message that
# might tell us how to handle it.
logger.error('Unsupported association type %s: %s' %
(assoc_type, server_error.error_text, ))
# Extract the session_type and assoc_type from the
# error message
assoc_type = server_error.message.getArg(OPENID_NS, 'assoc_type')
session_type = server_error.message.getArg(OPENID_NS, 'session_type')
if assoc_type is None or session_type is None:
logger.error('Server responded with unsupported association '
'session but did not supply a fallback.')
return None
elif not self.negotiator.isAllowed(assoc_type, session_type):
fmt = ('Server sent unsupported session/association type: '
'session_type=%s, assoc_type=%s')
logger.error(fmt % (session_type, assoc_type))
return None
else:
return assoc_type, session_type
def _requestAssociation(self, endpoint, assoc_type, session_type):
"""Make and process one association request to this endpoint's
OP endpoint URL.
@returns: An association object or None if the association
processing failed.
@raises ServerError: when the remote OpenID server returns an error.
"""
assoc_session, args = self._createAssociateRequest(
endpoint, assoc_type, session_type)
try:
response = self._makeKVPost(args, endpoint.server_url)
except fetchers.HTTPFetchingError as why:
logger.exception('openid.associate request failed: %s' % (why, ))
return None
try:
assoc = self._extractAssociation(response, assoc_session)
except KeyError as why:
logger.exception(
'Missing required parameter in response from %s: %s' %
(endpoint.server_url, why))
return None
except ProtocolError as why:
logger.exception('Protocol error parsing response from %s: %s' %
(endpoint.server_url, why))
return None
else:
return assoc
def _createAssociateRequest(self, endpoint, assoc_type, session_type):
"""Create an association request for the given assoc_type and
session_type.
@param endpoint: The endpoint whose server_url will be
queried. The important bit about the endpoint is whether
it's in compatiblity mode (OpenID 1.1)
@param assoc_type: The association type that the request
should ask for.
@type assoc_type: str
@param session_type: The session type that should be used in
the association request. The session_type is used to
create an association session object, and that session
object is asked for any additional fields that it needs to
add to the request.
@type session_type: str
@returns: a pair of the association session object and the
request message that will be sent to the server.
@rtype: (association session type (depends on session_type),
openid.message.Message)
"""
session_type_class = self.session_types[session_type]
assoc_session = session_type_class()
args = {
'mode': 'associate',
'assoc_type': assoc_type,
}
if not endpoint.compatibilityMode():
args['ns'] = OPENID2_NS
# Leave out the session type if we're in compatibility mode
# *and* it's no-encryption.
if (not endpoint.compatibilityMode() or
assoc_session.session_type != 'no-encryption'):
args['session_type'] = assoc_session.session_type
args.update(assoc_session.getRequest())
message = Message.fromOpenIDArgs(args)
return assoc_session, message
def _getOpenID1SessionType(self, assoc_response):
"""Given an association response message, extract the OpenID
1.X session type.
This function mostly takes care of the 'no-encryption' default
behavior in OpenID 1.
If the association type is plain-text, this function will
return 'no-encryption'
@returns: The association type for this message
@rtype: str
@raises KeyError: when the session_type field is absent.
"""
# If it's an OpenID 1 message, allow session_type to default
# to None (which signifies "no-encryption")
session_type = assoc_response.getArg(OPENID1_NS, 'session_type')
# Handle the differences between no-encryption association
# respones in OpenID 1 and 2:
# no-encryption is not really a valid session type for
# OpenID 1, but we'll accept it anyway, while issuing a
# warning.
if session_type == 'no-encryption':
logger.warning('OpenID server sent "no-encryption"'
'for OpenID 1.X')
# Missing or empty session type is the way to flag a
# 'no-encryption' response. Change the session type to
# 'no-encryption' so that it can be handled in the same
# way as OpenID 2 'no-encryption' respones.
elif session_type == '' or session_type is None:
session_type = 'no-encryption'
return session_type
def _extractAssociation(self, assoc_response, assoc_session):
"""Attempt to extract an association from the response, given
the association response message and the established
association session.
@param assoc_response: The association response message from
the server
@type assoc_response: openid.message.Message
@param assoc_session: The association session object that was
used when making the request
@type assoc_session: depends on the session type of the request
@raises ProtocolError: when data is malformed
@raises KeyError: when a field is missing
@rtype: openid.association.Association
"""
# Extract the common fields from the response, raising an
# exception if they are not found
assoc_type = assoc_response.getArg(OPENID_NS, 'assoc_type', no_default)
assoc_handle = assoc_response.getArg(OPENID_NS, 'assoc_handle',
no_default)
# expires_in is a base-10 string. The Python parsing will
# accept literals that have whitespace around them and will
# accept negative values. Neither of these are really in-spec,
# but we think it's OK to accept them.
expires_in_str = assoc_response.getArg(OPENID_NS, 'expires_in',
no_default)
try:
expires_in = int(expires_in_str)
except ValueError as why:
raise ProtocolError('Invalid expires_in field: %s' % (why, ))
# OpenID 1 has funny association session behaviour.
if assoc_response.isOpenID1():
session_type = self._getOpenID1SessionType(assoc_response)
else:
session_type = assoc_response.getArg(OPENID2_NS, 'session_type',
no_default)
# Session type mismatch
if assoc_session.session_type != session_type:
if (assoc_response.isOpenID1() and
session_type == 'no-encryption'):
# In OpenID 1, any association request can result in a
# 'no-encryption' association response. Setting
# assoc_session to a new no-encryption session should
# make the rest of this function work properly for
# that case.
assoc_session = PlainTextConsumerSession()
else:
# Any other mismatch, regardless of protocol version
# results in the failure of the association session
# altogether.
fmt = 'Session type mismatch. Expected %r, got %r'
message = fmt % (assoc_session.session_type, session_type)
raise ProtocolError(message)
# Make sure assoc_type is valid for session_type
if assoc_type not in assoc_session.allowed_assoc_types:
fmt = 'Unsupported assoc_type for session %s returned: %s'
raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
# Delegate to the association session to extract the secret
# from the response, however is appropriate for that session
# type.
try:
secret = assoc_session.extractSecret(assoc_response)
except ValueError as why:
fmt = 'Malformed response for %s session: %s'
raise ProtocolError(fmt % (assoc_session.session_type, why))
return Association.fromExpiresIn(expires_in, assoc_handle, secret,
assoc_type)
class AuthRequest(object):
"""An object that holds the state necessary for generating an
OpenID authentication request. This object holds the association
with the server and the discovered information with which the
request will be made.
It is separate from the consumer because you may wish to add
things to the request before sending it on its way to the
server. It also has serialization options that let you encode the
authentication request as a URL or as a form POST.
"""
def __init__(self, endpoint, assoc):
"""
Creates a new AuthRequest object. This just stores each
argument in an appropriately named field.
Users of this library should not create instances of this
class. Instances of this class are created by the library
when needed.
"""
self.assoc = assoc
self.endpoint = endpoint
self.return_to_args = {}
self.message = Message(endpoint.preferredNamespace())
self._anonymous = False
def setAnonymous(self, is_anonymous):
"""Set whether this request should be made anonymously. If a
request is anonymous, the identifier will not be sent in the
request. This is only useful if you are making another kind of
request with an extension in this request.
Anonymous requests are not allowed when the request is made
with OpenID 1.
@raises ValueError: when attempting to set an OpenID1 request
as anonymous
"""
if is_anonymous and self.message.isOpenID1():
raise ValueError('OpenID 1 requests MUST include the '
'identifier in the request')
else:
self._anonymous = is_anonymous
def addExtension(self, extension_request):
"""Add an extension to this checkid request.
@param extension_request: An object that implements the
extension interface for adding arguments to an OpenID
message.
"""
extension_request.toMessage(self.message)
def addExtensionArg(self, namespace, key, value):
"""Add an extension argument to this OpenID authentication
request.
Use caution when adding arguments, because they will be
URL-escaped and appended to the redirect URL, which can easily
get quite long.
@param namespace: The namespace for the extension. For
example, the simple registration extension uses the
namespace C{sreg}.
@type namespace: str
@param key: The key within the extension namespace. For
example, the nickname field in the simple registration
extension's key is C{nickname}.
@type key: str
@param value: The value to provide to the server for this
argument.
@type value: str
"""
self.message.setArg(namespace, key, value)
def getMessage(self, realm, return_to=None, immediate=False):
"""Produce a L{openid.message.Message} representing this request.
@param realm: The URL (or URL pattern) that identifies your
web site to the user when she is authorizing it.
@type realm: str
@param return_to: The URL that the OpenID provider will send the
user back to after attempting to verify her identity.
Not specifying a return_to URL means that the user will not
be returned to the site issuing the request upon its
completion.
@type return_to: str
@param immediate: If True, the OpenID provider is to send back
a response immediately, useful for behind-the-scenes
authentication attempts. Otherwise the OpenID provider
may engage the user before providing a response. This is
the default case, as the user may need to provide
credentials or approve the request before a positive
response can be sent.
@type immediate: bool
@returntype: L{openid.message.Message}
"""
if return_to:
return_to = oidutil.appendArgs(return_to, self.return_to_args)
elif immediate:
raise ValueError(
'"return_to" is mandatory when using "checkid_immediate"')
elif self.message.isOpenID1():
raise ValueError('"return_to" is mandatory for OpenID 1 requests')
elif self.return_to_args:
raise ValueError('extra "return_to" arguments were specified, '
'but no return_to was specified')
if immediate:
mode = 'checkid_immediate'
else:
mode = 'checkid_setup'
message = self.message.copy()
if message.isOpenID1():
realm_key = 'trust_root'
else:
realm_key = 'realm'
message.updateArgs(OPENID_NS, {
realm_key: realm,
'mode': mode,
'return_to': return_to,
})
if not self._anonymous:
if self.endpoint.isOPIdentifier():
# This will never happen when we're in compatibility
# mode, as long as isOPIdentifier() returns False
# whenever preferredNamespace() returns OPENID1_NS.
claimed_id = request_identity = IDENTIFIER_SELECT
else:
request_identity = self.endpoint.getLocalID()
claimed_id = self.endpoint.claimed_id
# This is true for both OpenID 1 and 2
message.setArg(OPENID_NS, 'identity', request_identity)
if message.isOpenID2():
message.setArg(OPENID2_NS, 'claimed_id', claimed_id)
if self.assoc:
message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle)
assoc_log_msg = 'with association %s' % (self.assoc.handle, )
else:
assoc_log_msg = 'using stateless mode.'
logger.info("Generated %s request to %s %s" %
(mode, self.endpoint.server_url, assoc_log_msg))
return message
def redirectURL(self, realm, return_to=None, immediate=False):
"""Returns a URL with an encoded OpenID request.
The resulting URL is the OpenID provider's endpoint URL with
parameters appended as query arguments. You should redirect
the user agent to this URL.
OpenID 2.0 endpoints also accept POST requests, see
C{L{shouldSendRedirect}} and C{L{formMarkup}}.
@param realm: The URL (or URL pattern) that identifies your
web site to the user when she is authorizing it.
@type realm: str
@param return_to: The URL that the OpenID provider will send the
user back to after attempting to verify her identity.
Not specifying a return_to URL means that the user will not
be returned to the site issuing the request upon its
completion.
@type return_to: str
@param immediate: If True, the OpenID provider is to send back
a response immediately, useful for behind-the-scenes
authentication attempts. Otherwise the OpenID provider
may engage the user before providing a response. This is
the default case, as the user may need to provide
credentials or approve the request before a positive
response can be sent.
@type immediate: bool
@returns: The URL to redirect the user agent to.
@returntype: str
"""
message = self.getMessage(realm, return_to, immediate)
return message.toURL(self.endpoint.server_url)
def formMarkup(self,
realm,
return_to=None,
immediate=False,
form_tag_attrs=None):
"""Get html for a form to submit this request to the IDP.
@param form_tag_attrs: Dictionary of attributes to be added to
the form tag. 'accept-charset' and 'enctype' have defaults
that can be overridden. If a value is supplied for
'action' or 'method', it will be replaced.
@type form_tag_attrs: {unicode: unicode}
"""
message = self.getMessage(realm, return_to, immediate)
return message.toFormMarkup(self.endpoint.server_url, form_tag_attrs)
def htmlMarkup(self,
realm,
return_to=None,
immediate=False,
form_tag_attrs=None):
"""Get an autosubmitting HTML page that submits this request to the
IDP. This is just a wrapper for formMarkup.
@see: formMarkup
@returns: str
"""
return oidutil.autoSubmitHTML(
self.formMarkup(realm, return_to, immediate, form_tag_attrs))
def shouldSendRedirect(self):
"""Should this OpenID authentication request be sent as a HTTP
redirect or as a POST (form submission)?
@rtype: bool
"""
return self.endpoint.compatibilityMode()
FAILURE = 'failure'
SUCCESS = 'success'
CANCEL = 'cancel'
SETUP_NEEDED = 'setup_needed'
class Response(object):
status = None
def setEndpoint(self, endpoint):
self.endpoint = endpoint
if endpoint is None:
self.identity_url = None
else:
self.identity_url = endpoint.claimed_id
def getDisplayIdentifier(self):
"""Return the display identifier for this response.
The display identifier is related to the Claimed Identifier, but the
two are not always identical. The display identifier is something the
user should recognize as what they entered, whereas the response's
claimed identifier (in the L{identity_url} attribute) may have extra
information for better persistence.
URLs will be stripped of their fragments for display. XRIs will
display the human-readable identifier (i-name) instead of the
persistent identifier (i-number).
Use the display identifier in your user interface. Use
L{identity_url} for querying your database or authorization server.
"""
if self.endpoint is not None:
return self.endpoint.getDisplayIdentifier()
return None
class SuccessResponse(Response):
"""A response with a status of SUCCESS. Indicates that this request is a
successful acknowledgement from the OpenID server that the
supplied URL is, indeed controlled by the requesting agent.
@ivar identity_url: The identity URL that has been authenticated;
the Claimed Identifier.
See also L{getDisplayIdentifier}.
@ivar endpoint: The endpoint that authenticated the identifier. You
may access other discovered information related to this endpoint,
such as the CanonicalID of an XRI, through this object.
@type endpoint:
L{OpenIDServiceEndpoint}
@ivar signed_fields: The arguments in the server's response that
were signed and verified.
@cvar status: SUCCESS
"""
status = SUCCESS
def __init__(self, endpoint, message, signed_fields=None):
# Don't use setEndpoint, because endpoint should never be None
# for a successfull transaction.
self.endpoint = endpoint
self.identity_url = endpoint.claimed_id
self.message = message
if signed_fields is None:
signed_fields = []
self.signed_fields = signed_fields
def isOpenID1(self):
"""Was this authentication response an OpenID 1 authentication
response?
"""
return self.message.isOpenID1()
def isSigned(self, ns_uri, ns_key):
"""Return whether a particular key is signed, regardless of
its namespace alias
"""
return self.message.getKey(ns_uri, ns_key) in self.signed_fields
def getSigned(self, ns_uri, ns_key, default=None):
"""Return the specified signed field if available,
otherwise return default
"""
if self.isSigned(ns_uri, ns_key):
return self.message.getArg(ns_uri, ns_key, default)
else:
return default
def getSignedNS(self, ns_uri):
"""Get signed arguments from the response message. Return a
dict of all arguments in the specified namespace. If any of
the arguments are not signed, return None.
"""
msg_args = self.message.getArgs(ns_uri)
for key in msg_args.keys():
if not self.isSigned(ns_uri, key):
logger.info(
"SuccessResponse.getSignedNS: (%s, %s) not signed." %
(ns_uri, key))
return None
return msg_args
def extensionResponse(self, namespace_uri, require_signed):
"""Return response arguments in the specified namespace.
@param namespace_uri: The namespace URI of the arguments to be
returned.
@param require_signed: True if the arguments should be among
those signed in the response, False if you don't care.
If require_signed is True and the arguments are not signed,
return None.
"""
if require_signed:
return self.getSignedNS(namespace_uri)
else:
return self.message.getArgs(namespace_uri)
def getReturnTo(self):
"""Get the openid.return_to argument from this response.
This is useful for verifying that this request was initiated
by this consumer.
@returns: The return_to URL supplied to the server on the
initial request, or C{None} if the response did not contain
an C{openid.return_to} argument.
@returntype: str
"""
return self.getSigned(OPENID_NS, 'return_to')
def __eq__(self, other):
return ((self.endpoint == other.endpoint) and
(self.identity_url == other.identity_url) and
(self.message == other.message) and
(self.signed_fields == other.signed_fields) and
(self.status == other.status))
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return '<%s.%s id=%r signed=%r>' % (
self.__class__.__module__, self.__class__.__name__,
self.identity_url, self.signed_fields)
class FailureResponse(Response):
"""A response with a status of FAILURE. Indicates that the OpenID
protocol has failed. This could be locally or remotely triggered.
@ivar identity_url: The identity URL for which authenitcation was
attempted, if it can be determined. Otherwise, None.
@ivar message: A message indicating why the request failed, if one
is supplied. otherwise, None.
@cvar status: FAILURE
"""
status = FAILURE
def __init__(self, endpoint, message=None, contact=None, reference=None):
self.setEndpoint(endpoint)
self.message = message
self.contact = contact
self.reference = reference
def __repr__(self):
return "<%s.%s id=%r message=%r>" % (self.__class__.__module__,
self.__class__.__name__,
self.identity_url, self.message)
class CancelResponse(Response):
"""A response with a status of CANCEL. Indicates that the user
cancelled the OpenID authentication request.
@ivar identity_url: The identity URL for which authenitcation was
attempted, if it can be determined. Otherwise, None.
@cvar status: CANCEL
"""
status = CANCEL
def __init__(self, endpoint):
self.setEndpoint(endpoint)
class SetupNeededResponse(Response):
"""A response with a status of SETUP_NEEDED. Indicates that the
request was in immediate mode, and the server is unable to
authenticate the user without further interaction.
@ivar identity_url: The identity URL for which authenitcation was
attempted.
@ivar setup_url: A URL that can be used to send the user to the
server to set up for authentication. The user should be
redirected in to the setup_url, either in the current window
or in a new browser window. C{None} in OpenID 2.0.
@cvar status: SETUP_NEEDED
"""
status = SETUP_NEEDED
def __init__(self, endpoint, setup_url=None):
self.setEndpoint(endpoint)
self.setup_url = setup_url
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/openid/consumer/discover.py 0000644 0001750 0001750 00000037456 00000000000 021617 0 ustar 00rami rami 0000000 0000000 # -*- test-case-name: openid.test.test_discover -*-
"""Functions to discover OpenID endpoints from identifiers.
"""
__all__ = [
'DiscoveryFailure',
'OPENID_1_0_NS',
'OPENID_1_0_TYPE',
'OPENID_1_1_TYPE',
'OPENID_2_0_TYPE',
'OPENID_IDP_2_0_TYPE',
'OpenIDServiceEndpoint',
'discover',
]
import urllib.parse
import logging
from openid import fetchers, urinorm
from openid import yadis
from openid.yadis.etxrd import nsTag, XRDSError, XRD_NS_2_0
from openid.yadis.services import applyFilter as extractServices
from openid.yadis.discover import discover as yadisDiscover
from openid.yadis.discover import DiscoveryFailure
from openid.yadis import xrires, filters
from openid.yadis import xri
from openid.consumer import html_parse
OPENID_1_0_NS = 'http://openid.net/xmlns/1.0'
OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server'
OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon'
OPENID_1_1_TYPE = 'http://openid.net/signon/1.1'
OPENID_1_0_TYPE = 'http://openid.net/signon/1.0'
from openid.message import OPENID1_NS as OPENID_1_0_MESSAGE_NS
from openid.message import OPENID2_NS as OPENID_2_0_MESSAGE_NS
logger = logging.getLogger(__name__)
class OpenIDServiceEndpoint(object):
"""Object representing an OpenID service endpoint.
@ivar identity_url: the verified identifier.
@ivar canonicalID: For XRI, the persistent identifier.
"""
# OpenID service type URIs, listed in order of preference. The
# ordering of this list affects yadis and XRI service discovery.
openid_type_uris = [
OPENID_IDP_2_0_TYPE,
OPENID_2_0_TYPE,
OPENID_1_1_TYPE,
OPENID_1_0_TYPE,
]
def __init__(self):
self.claimed_id = None
self.server_url = None
self.type_uris = []
self.local_id = None
self.canonicalID = None
self.used_yadis = False # whether this came from an XRDS
self.display_identifier = None
def usesExtension(self, extension_uri):
return extension_uri in self.type_uris
def preferredNamespace(self):
if (OPENID_IDP_2_0_TYPE in self.type_uris or
OPENID_2_0_TYPE in self.type_uris):
return OPENID_2_0_MESSAGE_NS
else:
return OPENID_1_0_MESSAGE_NS
def supportsType(self, type_uri):
"""Does this endpoint support this type?
I consider C{/server} endpoints to implicitly support C{/signon}.
"""
return ((type_uri in self.type_uris) or
(type_uri == OPENID_2_0_TYPE and self.isOPIdentifier()))
def getDisplayIdentifier(self):
"""Return the display_identifier if set, else return the claimed_id.
"""
if self.display_identifier is not None:
return self.display_identifier
if self.claimed_id is None:
return None
else:
return urllib.parse.urldefrag(self.claimed_id)[0]
def compatibilityMode(self):
return self.preferredNamespace() != OPENID_2_0_MESSAGE_NS
def isOPIdentifier(self):
return OPENID_IDP_2_0_TYPE in self.type_uris
def parseService(self, yadis_url, uri, type_uris, service_element):
"""Set the state of this object based on the contents of the
service element."""
self.type_uris = type_uris
self.server_url = uri
self.used_yadis = True
if not self.isOPIdentifier():
# XXX: This has crappy implications for Service elements
# that contain both 'server' and 'signon' Types. But
# that's a pathological configuration anyway, so I don't
# think I care.
self.local_id = findOPLocalIdentifier(service_element,
self.type_uris)
self.claimed_id = yadis_url
def getLocalID(self):
"""Return the identifier that should be sent as the
openid.identity parameter to the server."""
# I looked at this conditional and thought "ah-hah! there's the bug!"
# but Python actually makes that one big expression somehow, i.e.
# "x is x is x" is not the same thing as "(x is x) is x".
# That's pretty weird, dude. -- kmt, 1/07
if (self.local_id is self.canonicalID is None):
return self.claimed_id
else:
return self.local_id or self.canonicalID
def fromBasicServiceEndpoint(cls, endpoint):
"""Create a new instance of this class from the endpoint
object passed in.
@return: None or OpenIDServiceEndpoint for this endpoint object"""
type_uris = endpoint.matchTypes(cls.openid_type_uris)
# If any Type URIs match and there is an endpoint URI
# specified, then this is an OpenID endpoint
if type_uris and endpoint.uri is not None:
openid_endpoint = cls()
openid_endpoint.parseService(endpoint.yadis_url, endpoint.uri,
endpoint.type_uris,
endpoint.service_element)
else:
openid_endpoint = None
return openid_endpoint
fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint)
def fromHTML(cls, uri, html):
"""Parse the given document as HTML looking for an OpenID
@rtype: [OpenIDServiceEndpoint]
"""
discovery_types = [
(OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'),
(OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'),
]
link_attrs = html_parse.parseLinkAttrs(html)
services = []
for type_uri, op_endpoint_rel, local_id_rel in discovery_types:
op_endpoint_url = html_parse.findFirstHref(link_attrs,
op_endpoint_rel)
if op_endpoint_url is None:
continue
service = cls()
service.claimed_id = uri
service.local_id = html_parse.findFirstHref(link_attrs,
local_id_rel)
service.server_url = op_endpoint_url
service.type_uris = [type_uri]
services.append(service)
return services
fromHTML = classmethod(fromHTML)
def fromXRDS(cls, uri, xrds):
"""Parse the given document as XRDS looking for OpenID services.
@rtype: [OpenIDServiceEndpoint]
@raises XRDSError: When the XRDS does not parse.
@since: 2.1.0
"""
return extractServices(uri, xrds, cls)
fromXRDS = classmethod(fromXRDS)
def fromDiscoveryResult(cls, discoveryResult):
"""Create endpoints from a DiscoveryResult.
@type discoveryResult: L{DiscoveryResult}
@rtype: list of L{OpenIDServiceEndpoint}
@raises XRDSError: When the XRDS does not parse.
@since: 2.1.0
"""
if discoveryResult.isXRDS():
method = cls.fromXRDS
else:
method = cls.fromHTML
return method(discoveryResult.normalized_uri,
discoveryResult.response_text)
fromDiscoveryResult = classmethod(fromDiscoveryResult)
def fromOPEndpointURL(cls, op_endpoint_url):
"""Construct an OP-Identifier OpenIDServiceEndpoint object for
a given OP Endpoint URL
@param op_endpoint_url: The URL of the endpoint
@rtype: OpenIDServiceEndpoint
"""
service = cls()
service.server_url = op_endpoint_url
service.type_uris = [OPENID_IDP_2_0_TYPE]
return service
fromOPEndpointURL = classmethod(fromOPEndpointURL)
def __str__(self):
return ("<%s.%s "
"server_url=%r "
"claimed_id=%r "
"local_id=%r "
"canonicalID=%r "
"used_yadis=%s "
">" % (self.__class__.__module__, self.__class__.__name__,
self.server_url, self.claimed_id, self.local_id,
self.canonicalID, self.used_yadis))
def findOPLocalIdentifier(service_element, type_uris):
"""Find the OP-Local Identifier for this xrd:Service element.
This considers openid:Delegate to be a synonym for xrd:LocalID if
both OpenID 1.X and OpenID 2.0 types are present. If only OpenID
1.X is present, it returns the value of openid:Delegate. If only
OpenID 2.0 is present, it returns the value of xrd:LocalID. If
there is more than one LocalID tag and the values are different,
it raises a DiscoveryFailure. This is also triggered when the
xrd:LocalID and openid:Delegate tags are different.
@param service_element: The xrd:Service element
@type service_element: ElementTree.Node
@param type_uris: The xrd:Type values present in this service
element. This function could extract them, but higher level
code needs to do that anyway.
@type type_uris: [str]
@raises DiscoveryFailure: when discovery fails.
@returns: The OP-Local Identifier for this service element, if one
is present, or None otherwise.
@rtype: str or unicode or NoneType
"""
# XXX: Test this function on its own!
# Build the list of tags that could contain the OP-Local Identifier
local_id_tags = []
if (OPENID_1_1_TYPE in type_uris or OPENID_1_0_TYPE in type_uris):
local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate'))
if OPENID_2_0_TYPE in type_uris:
local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID'))
# Walk through all the matching tags and make sure that they all
# have the same value
local_id = None
for local_id_tag in local_id_tags:
for local_id_element in service_element.findall(local_id_tag):
if local_id is None:
local_id = local_id_element.text
elif local_id != local_id_element.text:
format = 'More than one %r tag found in one service element'
message = format % (local_id_tag, )
raise DiscoveryFailure(message, None)
return local_id
def normalizeURL(url):
"""Normalize a URL, converting normalization failures to
DiscoveryFailure"""
try:
normalized = urinorm.urinorm(url)
except ValueError as why:
raise DiscoveryFailure('Normalizing identifier: %s' % (why, ), None)
else:
return urllib.parse.urldefrag(normalized)[0]
def normalizeXRI(xri):
"""Normalize an XRI, stripping its scheme if present"""
if xri.startswith("xri://"):
xri = xri[6:]
return xri
def arrangeByType(service_list, preferred_types):
"""Rearrange service_list in a new list so services are ordered by
types listed in preferred_types. Return the new list."""
def enumerate(elts):
"""Return an iterable that pairs the index of an element with
that element.
For Python 2.2 compatibility"""
return list(zip(list(range(len(elts))), elts))
def bestMatchingService(service):
"""Return the index of the first matching type, or something
higher if no type matches.
This provides an ordering in which service elements that
contain a type that comes earlier in the preferred types list
come before service elements that come later. If a service
element has more than one type, the most preferred one wins.
"""
for i, t in enumerate(preferred_types):
if preferred_types[i] in service.type_uris:
return i
return len(preferred_types)
# Build a list with the service elements in tuples whose
# comparison will prefer the one with the best matching service
prio_services = [(bestMatchingService(s), orig_index, s)
for (orig_index, s) in enumerate(service_list)]
prio_services.sort()
# Now that the services are sorted by priority, remove the sort
# keys from the list.
for i in range(len(prio_services)):
prio_services[i] = prio_services[i][2]
return prio_services
def getOPOrUserServices(openid_services):
"""Extract OP Identifier services. If none found, return the
rest, sorted with most preferred first according to
OpenIDServiceEndpoint.openid_type_uris.
openid_services is a list of OpenIDServiceEndpoint objects.
Returns a list of OpenIDServiceEndpoint objects."""
op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE])
openid_services = arrangeByType(openid_services,
OpenIDServiceEndpoint.openid_type_uris)
return op_services or openid_services
def discoverYadis(uri):
"""Discover OpenID services for a URI. Tries Yadis and falls back
on old-style discovery if Yadis fails.
@param uri: normalized identity URL
@type uri: str
@return: (claimed_id, services)
@rtype: (str, list(OpenIDServiceEndpoint))
@raises DiscoveryFailure: when discovery fails.
"""
# Might raise a yadis.discover.DiscoveryFailure if no document
# came back for that URI at all. I don't think falling back
# to OpenID 1.0 discovery on the same URL will help, so don't
# bother to catch it.
response = yadisDiscover(uri)
yadis_url = response.normalized_uri
body = response.response_text
try:
openid_services = OpenIDServiceEndpoint.fromXRDS(yadis_url, body)
except XRDSError:
# Does not parse as a Yadis XRDS file
openid_services = []
if not openid_services:
# Either not an XRDS or there are no OpenID services.
if response.isXRDS():
# if we got the Yadis content-type or followed the Yadis
# header, re-fetch the document without following the Yadis
# header, with no Accept header.
return discoverNoYadis(uri)
# Try to parse the response as HTML.
#
openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body)
return (yadis_url, getOPOrUserServices(openid_services))
def discoverXRI(iname):
endpoints = []
iname = normalizeXRI(iname)
try:
canonicalID, services = xrires.ProxyResolver().query(
iname, OpenIDServiceEndpoint.openid_type_uris)
if canonicalID is None:
raise XRDSError('No CanonicalID found for XRI %r' % (iname, ))
flt = filters.mkFilter(OpenIDServiceEndpoint)
for service_element in services:
endpoints.extend(flt.getServiceEndpoints(iname, service_element))
except XRDSError:
logger.exception('xrds error on ' + iname)
for endpoint in endpoints:
# Is there a way to pass this through the filter to the endpoint
# constructor instead of tacking it on after?
endpoint.canonicalID = canonicalID
endpoint.claimed_id = canonicalID
endpoint.display_identifier = iname
# FIXME: returned xri should probably be in some normal form
return iname, getOPOrUserServices(endpoints)
def discoverNoYadis(uri):
http_resp = fetchers.fetch(uri)
if http_resp.status not in (200, 206):
raise DiscoveryFailure(
'HTTP Response status from identity URL host is not 200. '
'Got status %r' % (http_resp.status, ), http_resp)
claimed_id = http_resp.final_url
openid_services = OpenIDServiceEndpoint.fromHTML(claimed_id,
http_resp.body)
return claimed_id, openid_services
def discoverURI(uri):
parsed = urllib.parse.urlparse(uri)
if parsed[0] and parsed[1]:
if parsed[0] not in ['http', 'https']:
raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None)
else:
uri = 'http://' + uri
uri = normalizeURL(uri)
claimed_id, openid_services = discoverYadis(uri)
claimed_id = normalizeURL(claimed_id)
return claimed_id, openid_services
def discover(identifier):
if xri.identifierScheme(identifier) == "XRI":
return discoverXRI(identifier)
else:
return discoverURI(identifier)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/consumer/html_parse.py 0000644 0001750 0001750 00000017474 00000000000 022135 0 ustar 00rami rami 0000000 0000000 """
This module implements a VERY limited parser that finds tags in
the head of HTML or XHTML documents and parses out their attributes
according to the OpenID spec. It is a liberal parser, but it requires
these things from the data in order to work:
- There must be an open tag
- There must be an open tag inside of the tag
- Only s that are found inside of the tag are parsed
(this is by design)
- The parser follows the OpenID specification in resolving the
attributes of the link tags. This means that the attributes DO NOT
get resolved as they would by an XML or HTML parser. In particular,
only certain entities get replaced, and href attributes do not get
resolved relative to a base URL.
From http://openid.net/specs.bml#linkrel:
- The openid.server URL MUST be an absolute URL. OpenID consumers
MUST NOT attempt to resolve relative URLs.
- The openid.server URL MUST NOT include entities other than &,
<, >, and ".
The parser ignores SGML comments and . Both kinds of
quoting are allowed for attributes.
The parser deals with invalid markup in these ways:
- Tag names are not case-sensitive
- The tag is accepted even when it is not at the top level
- The tag is accepted even when it is not a direct child of
the tag, but a tag must be an ancestor of the
tag
- tags are accepted even when they are not direct children of
the tag, but a tag must be an ancestor of the
tag
- If there is no closing tag for an open or tag, the
remainder of the document is viewed as being inside of the tag. If
there is no closing tag for a tag, the link tag is treated
as a short tag. Exceptions to this rule are that closes
and or closes
- Attributes of the tag are not required to be quoted.
- In the case of duplicated attribute names, the attribute coming
last in the tag will be the value returned.
- Any text that does not parse as an attribute within a link tag will
be ignored. (e.g. will ignore
pumpkin)
- If there are more than one or tag, the parser only
looks inside of the first one.
- The contents of
''', flags)
tag_expr = r'''
# Starts with the tag name at a word boundary, where the tag name is
# not a namespace
<%(tag_name)s\b(?!:)
# All of the stuff up to a ">", hopefully attributes.
(?P[^>]*?)
(?: # Match a short tag
/>
| # Match a full tag
>
(?P.*?)
# Closed by
(?: # One of the specified close tags
?%(closers)s\s*>
# End of the string
| \Z
)
)
'''
def tagMatcher(tag_name, *close_tags):
if close_tags:
options = '|'.join((tag_name, ) + close_tags)
closers = '(?:%s)' % (options, )
else:
closers = tag_name
expr = tag_expr % locals()
return re.compile(expr, flags)
# Must contain at least an open html and an open head tag
html_find = tagMatcher('html')
head_find = tagMatcher('head', 'body')
link_find = re.compile(r'\w+)=
# Then either a quoted or unquoted attribute
(?:
# Match everything that\'s between matching quote marks
(?P["\'])(?P.*?)(?P=qopen)
|
# If the value is not quoted, match up to whitespace
(?P(?:[^\s<>/]|/(?!>))+)
)
|
(?P[<>])
''', flags)
# Entity replacement:
replacements = {
'amp': '&',
'lt': '<',
'gt': '>',
'quot': '"',
}
ent_replace = re.compile(r'&(%s);' % '|'.join(list(replacements.keys())))
def replaceEnt(mo):
"Replace the entities that are specified by OpenID"
return replacements.get(mo.group(1), mo.group())
def parseLinkAttrs(html, ignore_errors=False):
"""Find all link tags in a string representing a HTML document and
return a list of their attributes.
@param html: the text to parse
@type html: str or unicode
@param ignore_errors: whether to return despite e.g. parsing errors
@type ignore_errors: bool
@return: A list of dictionaries of attributes, one for each link tag
@rtype: [[(type(html), type(html))]]
"""
if isinstance(html, bytes):
# Attempt to decode as UTF-8, since that's the most modern -- also
# try Latin-1, since that's suggested by HTTP/1.1. If neither of
# those works, fall over.
try:
html = html.decode("utf-8")
except UnicodeDecodeError:
try:
html = html.decode("latin1")
except UnicodeDecodeError:
if ignore_errors:
# Optionally ignore the errors and act as if no link attrs
# were found here
return []
else:
raise AssertionError("Unreadable HTML!")
stripped = removed_re.sub('', html)
html_mo = html_find.search(stripped)
if html_mo is None or html_mo.start('contents') == -1:
return []
start, end = html_mo.span('contents')
head_mo = head_find.search(stripped, start, end)
if head_mo is None or head_mo.start('contents') == -1:
return []
start, end = head_mo.span('contents')
link_mos = link_find.finditer(stripped, head_mo.start(), head_mo.end())
matches = []
for link_mo in link_mos:
start = link_mo.start() + 5
link_attrs = {}
for attr_mo in attr_find.finditer(stripped, start):
if attr_mo.lastgroup == 'end_link':
break
# Either q_val or unq_val must be present, but not both
# unq_val is a True (non-empty) value if it is present
attr_name, q_val, unq_val = attr_mo.group('attr_name', 'q_val',
'unq_val')
attr_val = ent_replace.sub(replaceEnt, unq_val or q_val)
link_attrs[attr_name] = attr_val
matches.append(link_attrs)
return matches
def relMatches(rel_attr, target_rel):
"""Does this target_rel appear in the rel_str?"""
# XXX: TESTME
rels = rel_attr.strip().split()
for rel in rels:
rel = rel.lower()
if rel == target_rel:
return 1
return 0
def linkHasRel(link_attrs, target_rel):
"""Does this link have target_rel as a relationship?"""
# XXX: TESTME
rel_attr = link_attrs.get('rel')
return rel_attr and relMatches(rel_attr, target_rel)
def findLinksRel(link_attrs_list, target_rel):
"""Filter the list of link attributes on whether it has target_rel
as a relationship."""
# XXX: TESTME
matchesTarget = lambda attrs: linkHasRel(attrs, target_rel)
return list(filter(matchesTarget, link_attrs_list))
def findFirstHref(link_attrs_list, target_rel):
"""Return the value of the href attribute for the first link tag
in the list that has target_rel as a relationship."""
# XXX: TESTME
matches = findLinksRel(link_attrs_list, target_rel)
if not matches:
return None
first = matches[0]
return first.get('href')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/cryptutil.py 0000644 0001750 0001750 00000007212 00000000000 020170 0 ustar 00rami rami 0000000 0000000 """Module containing a cryptographic-quality source of randomness and
other cryptographically useful functionality
Python 2.4 needs no external support for this module, nor does Python
2.3 on a system with /dev/urandom.
Other configurations will need a quality source of random bytes and
access to a function that will convert binary strings to long
integers. This module will work with the Python Cryptography Toolkit
(pycrypto) if it is present. pycrypto can be found with a search
engine, but is currently found at:
http://www.amk.ca/python/code/crypto
"""
__all__ = [
'base64ToLong',
'binaryToLong',
'hmacSha1',
'hmacSha256',
'longToBase64',
'longToBinary',
'randomString',
'randrange',
'sha1',
'sha256',
]
import hmac
import os
import random
from openid.oidutil import toBase64, fromBase64
import hashlib
class HashContainer(object):
def __init__(self, hash_constructor):
self.new = hash_constructor
self.digest_size = hash_constructor().digest_size
sha1_module = HashContainer(hashlib.sha1)
sha256_module = HashContainer(hashlib.sha256)
def hmacSha1(key, text):
if isinstance(key, str):
key = bytes(key, encoding="utf-8")
if isinstance(text, str):
text = bytes(text, encoding="utf-8")
return hmac.new(key, text, sha1_module).digest()
def sha1(s):
if isinstance(s, str):
s = bytes(s, encoding="utf-8")
return sha1_module.new(s).digest()
def hmacSha256(key, text):
if isinstance(key, str):
key = bytes(key, encoding="utf-8")
if isinstance(text, str):
text = bytes(text, encoding="utf-8")
return hmac.new(key, text, sha256_module).digest()
def sha256(s):
if isinstance(s, str):
s = bytes(s, encoding="utf-8")
return sha256_module.new(s).digest()
SHA256_AVAILABLE = True
try:
from Crypto.Util.number import long_to_bytes, bytes_to_long
except ImportError:
# In the case where we don't have pycrypto installed, define substitute
# functionality.
import pickle
def longToBinary(l):
if l == 0:
return b'\x00'
b = bytearray(pickle.encode_long(l))
b.reverse()
return bytes(b)
def binaryToLong(s):
if isinstance(s, str):
s = s.encode("utf-8")
b = bytearray(s)
b.reverse()
return pickle.decode_long(bytes(b))
else:
# We have pycrypto, so wrap its functions instead.
def longToBinary(l):
if l < 0:
raise ValueError('This function only supports positive integers')
bytestring = long_to_bytes(l)
if bytestring[0] > 127:
return b'\x00' + bytestring
else:
return bytestring
def binaryToLong(bytestring):
if not bytestring:
raise ValueError('Empty string passed to strToLong')
if bytestring[0] > 127:
raise ValueError('This function only supports positive integers')
return bytes_to_long(bytestring)
# A cryptographically safe source of random bytes
getBytes = os.urandom
# A randrange function that works for longs
randrange = random.randrange
def longToBase64(l):
return toBase64(longToBinary(l))
def base64ToLong(s):
return binaryToLong(fromBase64(s))
def randomString(length, chrs=None):
"""Produce a string of length random bytes, chosen from chrs."""
if chrs is None:
return getBytes(length)
else:
n = len(chrs)
return ''.join([chrs[randrange(n)] for _ in range(length)])
def const_eq(s1, s2):
if len(s1) != len(s2):
return False
result = True
for i in range(len(s1)):
result = result and (s1[i] == s2[i])
return result
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/dh.py 0000644 0001750 0001750 00000003171 00000000000 016524 0 ustar 00rami rami 0000000 0000000 from openid import cryptutil
def strxor(x, y):
if len(x) != len(y):
raise ValueError('Inputs to strxor must have the same length')
if isinstance(x, str):
x = x.encode("utf-8")
if isinstance(y, str):
y = y.encode("utf-8")
return bytes([a ^ b for a, b in zip(x, y)])
class DiffieHellman(object):
DEFAULT_MOD = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443
DEFAULT_GEN = 2
def fromDefaults(cls):
return cls(cls.DEFAULT_MOD, cls.DEFAULT_GEN)
fromDefaults = classmethod(fromDefaults)
def __init__(self, modulus, generator):
self.modulus = int(modulus)
self.generator = int(generator)
self._setPrivate(cryptutil.randrange(1, modulus - 1))
def _setPrivate(self, private):
"""This is here to make testing easier"""
self.private = private
self.public = pow(self.generator, self.private, self.modulus)
def usingDefaultValues(self):
return (self.modulus == self.DEFAULT_MOD and
self.generator == self.DEFAULT_GEN)
def getSharedSecret(self, composite):
return pow(composite, self.private, self.modulus)
def xorSecret(self, composite, secret, hash_func):
dh_shared = self.getSharedSecret(composite)
hashed_dh_shared = hash_func(cryptutil.longToBinary(dh_shared))
return strxor(secret, hashed_dh_shared)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/extension.py 0000644 0001750 0001750 00000003240 00000000000 020142 0 ustar 00rami rami 0000000 0000000 import warnings
from openid import message as message_module
class Extension(object):
"""An interface for OpenID extensions.
@ivar ns_uri: The namespace to which to add the arguments for this
extension
"""
ns_uri = None
ns_alias = None
def getExtensionArgs(self):
"""Get the string arguments that should be added to an OpenID
message for this extension.
@returns: A dictionary of completely non-namespaced arguments
to be added. For example, if the extension's alias is
'uncle', and this method returns {'meat':'Hot Rats'}, the
final message will contain {'openid.uncle.meat':'Hot Rats'}
"""
raise NotImplementedError()
def toMessage(self, message=None):
"""Add the arguments from this extension to the provided
message, or create a new message containing only those
arguments.
@returns: The message with the extension arguments added
"""
if message is None:
warnings.warn(
'Passing None to Extension.toMessage is deprecated. '
'Creating a message assuming you want OpenID 2.',
DeprecationWarning,
stacklevel=2)
message = message_module.Message(message_module.OPENID2_NS)
implicit = message.isOpenID1()
try:
message.namespaces.addAlias(
self.ns_uri, self.ns_alias, implicit=implicit)
except KeyError:
if message.namespaces.getAlias(self.ns_uri) != self.ns_alias:
raise
message.updateArgs(self.ns_uri, self.getExtensionArgs())
return message
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8718696
python3-openid-3.2.0/openid/extensions/ 0000755 0001750 0001750 00000000000 00000000000 017754 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/extensions/__init__.py 0000644 0001750 0001750 00000000165 00000000000 022067 0 ustar 00rami rami 0000000 0000000 """OpenID Extension modules."""
__all__ = ['ax', 'pape', 'sreg']
from openid.extensions.draft import pape5 as pape
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/extensions/ax.py 0000644 0001750 0001750 00000064005 00000000000 020743 0 ustar 00rami rami 0000000 0000000 # -*- test-case-name: openid.test.test_ax -*-
"""Implements the OpenID Attribute Exchange specification, version 1.0.
@since: 2.1.0
"""
__all__ = [
'AttributeRequest',
'FetchRequest',
'FetchResponse',
'StoreRequest',
'StoreResponse',
]
from openid import extension
from openid.server.trustroot import TrustRoot
from openid.message import NamespaceMap, OPENID_NS
# Use this as the 'count' value for an attribute in a FetchRequest to
# ask for as many values as the OP can provide.
UNLIMITED_VALUES = "unlimited"
# Minimum supported alias length in characters. Here for
# completeness.
MINIMUM_SUPPORTED_ALIAS_LENGTH = 32
def checkAlias(alias):
"""
Check an alias for invalid characters; raise AXError if any are
found. Return None if the alias is valid.
"""
if ',' in alias:
raise AXError("Alias %r must not contain comma" % (alias, ))
if '.' in alias:
raise AXError("Alias %r must not contain period" % (alias, ))
class AXError(ValueError):
"""Results from data that does not meet the attribute exchange 1.0
specification"""
class NotAXMessage(AXError):
"""Raised when there is no Attribute Exchange mode in the message."""
def __repr__(self):
return self.__class__.__name__
def __str__(self):
return self.__class__.__name__
class AXMessage(extension.Extension):
"""Abstract class containing common code for attribute exchange messages
@cvar ns_alias: The preferred namespace alias for attribute
exchange messages
@cvar mode: The type of this attribute exchange message. This must
be overridden in subclasses.
"""
# This class is abstract, so it's OK that it doesn't override the
# abstract method in Extension:
#
#pylint:disable-msg=W0223
ns_alias = 'ax'
ns_uri = 'http://openid.net/srv/ax/1.0'
mode = None # NOTE mode is only ever set to a str value, see below
def _checkMode(self, ax_args):
"""Raise an exception if the mode in the attribute exchange
arguments does not match what is expected for this class.
@raises NotAXMessage: When there is no mode value in ax_args at all.
@raises AXError: When mode does not match.
"""
mode = ax_args.get('mode')
if isinstance(mode, bytes):
mode = str(mode, encoding="utf-8")
if mode != self.mode:
if not mode:
raise NotAXMessage()
else:
raise AXError('Expected mode %r; got %r' % (self.mode, mode))
def _newArgs(self):
"""Return a set of attribute exchange arguments containing the
basic information that must be in every attribute exchange
message.
"""
return {'mode': self.mode}
class AttrInfo(object):
"""Represents a single attribute in an attribute exchange
request. This should be added to an AXRequest object in order to
request the attribute.
@ivar required: Whether the attribute will be marked as required
when presented to the subject of the attribute exchange
request.
@type required: bool
@ivar count: How many values of this type to request from the
subject. Defaults to one.
@type count: int
@ivar type_uri: The identifier that determines what the attribute
represents and how it is serialized. For example, one type URI
representing dates could represent a Unix timestamp in base 10
and another could represent a human-readable string.
@type type_uri: str
@ivar alias: The name that should be given to this alias in the
request. If it is not supplied, a generic name will be
assigned. For example, if you want to call a Unix timestamp
value 'tstamp', set its alias to that value. If two attributes
in the same message request to use the same alias, the request
will fail to be generated.
@type alias: str or NoneType
"""
# It's OK that this class doesn't have public methods (it's just a
# holder for a bunch of attributes):
#
#pylint:disable-msg=R0903
def __init__(self, type_uri, count=1, required=False, alias=None):
self.required = required
self.count = count
self.type_uri = type_uri
self.alias = alias
if self.alias is not None:
checkAlias(self.alias)
def wantsUnlimitedValues(self):
"""
When processing a request for this attribute, the OP should
call this method to determine whether all available attribute
values were requested. If self.count == UNLIMITED_VALUES,
this returns True. Otherwise this returns False, in which
case self.count is an integer.
"""
return self.count == UNLIMITED_VALUES
def toTypeURIs(namespace_map, alias_list_s):
"""Given a namespace mapping and a string containing a
comma-separated list of namespace aliases, return a list of type
URIs that correspond to those aliases.
@param namespace_map: The mapping from namespace URI to alias
@type namespace_map: openid.message.NamespaceMap
@param alias_list_s: The string containing the comma-separated
list of aliases. May also be None for convenience.
@type alias_list_s: str or NoneType
@returns: The list of namespace URIs that corresponds to the
supplied list of aliases. If the string was zero-length or
None, an empty list will be returned.
@raise KeyError: If an alias is present in the list of aliases but
is not present in the namespace map.
"""
uris = []
if alias_list_s:
for alias in alias_list_s.split(','):
type_uri = namespace_map.getNamespaceURI(alias)
if type_uri is None:
raise KeyError('No type is defined for attribute name %r' %
(alias, ))
else:
uris.append(type_uri)
return uris
class FetchRequest(AXMessage):
"""An attribute exchange 'fetch_request' message. This message is
sent by a relying party when it wishes to obtain attributes about
the subject of an OpenID authentication request.
@ivar requested_attributes: The attributes that have been
requested thus far, indexed by the type URI.
@type requested_attributes: {str:AttrInfo}
@ivar update_url: A URL that will accept responses for this
attribute exchange request, even in the absence of the user
who made this request.
"""
mode = 'fetch_request'
def __init__(self, update_url=None):
AXMessage.__init__(self)
self.requested_attributes = {}
self.update_url = update_url
def add(self, attribute):
"""Add an attribute to this attribute exchange request.
@param attribute: The attribute that is being requested
@type attribute: C{L{AttrInfo}}
@returns: None
@raise KeyError: when the requested attribute is already
present in this fetch request.
"""
if attribute.type_uri in self.requested_attributes:
raise KeyError('The attribute %r has already been requested' %
(attribute.type_uri, ))
self.requested_attributes[attribute.type_uri] = attribute
def getExtensionArgs(self):
"""Get the serialized form of this attribute fetch request.
@returns: The fetch request message parameters
@rtype: {unicode:unicode}
"""
aliases = NamespaceMap()
required = []
if_available = []
ax_args = self._newArgs()
for type_uri, attribute in self.requested_attributes.items():
if attribute.alias is None:
alias = aliases.add(type_uri)
else:
# This will raise an exception when the second
# attribute with the same alias is added. I think it
# would be better to complain at the time that the
# attribute is added to this object so that the code
# that is adding it is identified in the stack trace,
# but it's more work to do so, and it won't be 100%
# accurate anyway, since the attributes are
# mutable. So for now, just live with the fact that
# we'll learn about the error later.
#
# The other possible approach is to hide the error and
# generate a new alias on the fly. I think that would
# probably be bad.
alias = aliases.addAlias(type_uri, attribute.alias)
if attribute.required:
required.append(alias)
else:
if_available.append(alias)
if attribute.count != 1:
ax_args['count.' + alias] = str(attribute.count)
ax_args['type.' + alias] = type_uri
if required:
ax_args['required'] = ','.join(required)
if if_available:
ax_args['if_available'] = ','.join(if_available)
return ax_args
def getRequiredAttrs(self):
"""Get the type URIs for all attributes that have been marked
as required.
@returns: A list of the type URIs for attributes that have
been marked as required.
@rtype: [str]
"""
required = []
for type_uri, attribute in self.requested_attributes.items():
if attribute.required:
required.append(type_uri)
return required
def fromOpenIDRequest(cls, openid_request):
"""Extract a FetchRequest from an OpenID message
@param openid_request: The OpenID authentication request
containing the attribute fetch request
@type openid_request: C{L{openid.server.server.CheckIDRequest}}
@rtype: C{L{FetchRequest}} or C{None}
@returns: The FetchRequest extracted from the message or None, if
the message contained no AX extension.
@raises KeyError: if the AuthRequest is not consistent in its use
of namespace aliases.
@raises AXError: When parseExtensionArgs would raise same.
@see: L{parseExtensionArgs}
"""
message = openid_request.message
ax_args = message.getArgs(cls.ns_uri)
self = cls()
try:
self.parseExtensionArgs(ax_args)
except NotAXMessage as err:
return None
if self.update_url:
# Update URL must match the openid.realm of the underlying
# OpenID 2 message.
realm = message.getArg(OPENID_NS, 'realm',
message.getArg(OPENID_NS, 'return_to'))
if not realm:
raise AXError(
("Cannot validate update_url %r " + "against absent realm")
% (self.update_url, ))
tr = TrustRoot.parse(realm)
if not tr.validateURL(self.update_url):
raise AXError(
"Update URL %r failed validation against realm %r" %
(self.update_url, realm, ))
return self
fromOpenIDRequest = classmethod(fromOpenIDRequest)
def parseExtensionArgs(self, ax_args):
"""Given attribute exchange arguments, populate this FetchRequest.
@param ax_args: Attribute Exchange arguments from the request.
As returned from L{Message.getArgs}.
@type ax_args: dict
@raises KeyError: if the message is not consistent in its use
of namespace aliases.
@raises NotAXMessage: If ax_args does not include an Attribute Exchange
mode.
@raises AXError: If the data to be parsed does not follow the
attribute exchange specification. At least when
'if_available' or 'required' is not specified for a
particular attribute type.
"""
# Raises an exception if the mode is not the expected value
self._checkMode(ax_args)
aliases = NamespaceMap()
for key, value in ax_args.items():
if key.startswith('type.'):
alias = key[5:]
type_uri = value
aliases.addAlias(type_uri, alias)
count_key = 'count.' + alias
count_s = ax_args.get(count_key)
if count_s:
try:
count = int(count_s)
if count <= 0:
raise AXError(
"Count %r must be greater than zero, got %r" %
(count_key, count_s, ))
except ValueError:
if count_s != UNLIMITED_VALUES:
raise AXError("Invalid count value for %r: %r" %
(count_key, count_s, ))
count = count_s
else:
count = 1
self.add(AttrInfo(type_uri, alias=alias, count=count))
required = toTypeURIs(aliases, ax_args.get('required'))
for type_uri in required:
self.requested_attributes[type_uri].required = True
if_available = toTypeURIs(aliases, ax_args.get('if_available'))
all_type_uris = required + if_available
for type_uri in aliases.iterNamespaceURIs():
if type_uri not in all_type_uris:
raise AXError('Type URI %r was in the request but not '
'present in "required" or "if_available"' %
(type_uri, ))
self.update_url = ax_args.get('update_url')
def iterAttrs(self):
"""Iterate over the AttrInfo objects that are
contained in this fetch_request.
"""
return iter(self.requested_attributes.values())
def __iter__(self):
"""Iterate over the attribute type URIs in this fetch_request
"""
return iter(self.requested_attributes)
def has_key(self, type_uri):
"""Is the given type URI present in this fetch_request?
"""
return type_uri in self.requested_attributes
__contains__ = has_key
class AXKeyValueMessage(AXMessage):
"""An abstract class that implements a message that has attribute
keys and values. It contains the common code between
fetch_response and store_request.
"""
# This class is abstract, so it's OK that it doesn't override the
# abstract method in Extension:
#
#pylint:disable-msg=W0223
def __init__(self):
AXMessage.__init__(self)
self.data = {}
def addValue(self, type_uri, value):
"""Add a single value for the given attribute type to the
message. If there are already values specified for this type,
this value will be sent in addition to the values already
specified.
@param type_uri: The URI for the attribute
@param value: The value to add to the response to the relying
party for this attribute
@type value: unicode
@returns: None
"""
try:
values = self.data[type_uri]
except KeyError:
values = self.data[type_uri] = []
values.append(value)
def setValues(self, type_uri, values):
"""Set the values for the given attribute type. This replaces
any values that have already been set for this attribute.
@param type_uri: The URI for the attribute
@param values: A list of values to send for this attribute.
@type values: [unicode]
"""
self.data[type_uri] = values
def _getExtensionKVArgs(self, aliases=None):
"""Get the extension arguments for the key/value pairs
contained in this message.
@param aliases: An alias mapping. Set to None if you don't
care about the aliases for this request.
"""
if aliases is None:
aliases = NamespaceMap()
ax_args = {}
for type_uri, values in self.data.items():
alias = aliases.add(type_uri)
ax_args['type.' + alias] = type_uri
ax_args['count.' + alias] = str(len(values))
for i, value in enumerate(values):
key = 'value.%s.%d' % (alias, i + 1)
ax_args[key] = value
return ax_args
def parseExtensionArgs(self, ax_args):
"""Parse attribute exchange key/value arguments into this
object.
@param ax_args: The attribute exchange fetch_response
arguments, with namespacing removed.
@type ax_args: {unicode:unicode}
@returns: None
@raises ValueError: If the message has bad values for
particular fields
@raises KeyError: If the namespace mapping is bad or required
arguments are missing
"""
self._checkMode(ax_args)
aliases = NamespaceMap()
for key, value in ax_args.items():
if key.startswith('type.'):
type_uri = value
alias = key[5:]
checkAlias(alias)
aliases.addAlias(type_uri, alias)
for type_uri, alias in aliases.items():
try:
count_s = ax_args['count.' + alias]
except KeyError:
value = ax_args['value.' + alias]
if value == '':
values = []
else:
values = [value]
else:
count = int(count_s)
values = []
for i in range(1, count + 1):
value_key = 'value.%s.%d' % (alias, i)
value = ax_args[value_key]
values.append(value)
self.data[type_uri] = values
def getSingle(self, type_uri, default=None):
"""Get a single value for an attribute. If no value was sent
for this attribute, use the supplied default. If there is more
than one value for this attribute, this method will fail.
@type type_uri: str
@param type_uri: The URI for the attribute
@param default: The value to return if the attribute was not
sent in the fetch_response.
@returns: The value of the attribute in the fetch_response
message, or the default supplied
@rtype: unicode or NoneType
@raises ValueError: If there is more than one value for this
parameter in the fetch_response message.
@raises KeyError: If the attribute was not sent in this response
"""
values = self.data.get(type_uri)
if not values:
return default
elif len(values) == 1:
return values[0]
else:
raise AXError('More than one value present for %r' % (type_uri, ))
def get(self, type_uri):
"""Get the list of values for this attribute in the
fetch_response.
XXX: what to do if the values are not present? default
parameter? this is funny because it's always supposed to
return a list, so the default may break that, though it's
provided by the user's code, so it might be okay. If no
default is supplied, should the return be None or []?
@param type_uri: The URI of the attribute
@returns: The list of values for this attribute in the
response. May be an empty list.
@rtype: [unicode]
@raises KeyError: If the attribute was not sent in the response
"""
return self.data[type_uri]
def count(self, type_uri):
"""Get the number of responses for a particular attribute in
this fetch_response message.
@param type_uri: The URI of the attribute
@returns: The number of values sent for this attribute
@raises KeyError: If the attribute was not sent in the
response. KeyError will not be raised if the number of
values was zero.
"""
return len(self.get(type_uri))
class FetchResponse(AXKeyValueMessage):
"""A fetch_response attribute exchange message
"""
mode = 'fetch_response'
def __init__(self, request=None, update_url=None):
"""
@param request: When supplied, I will use namespace aliases
that match those in this request. I will also check to
make sure I do not respond with attributes that were not
requested.
@type request: L{FetchRequest}
@param update_url: By default, C{update_url} is taken from the
request. But if you do not supply the request, you may set
the C{update_url} here.
@type update_url: str
"""
AXKeyValueMessage.__init__(self)
self.update_url = update_url
self.request = request
def getExtensionArgs(self):
"""Serialize this object into arguments in the attribute
exchange namespace
@returns: The dictionary of unqualified attribute exchange
arguments that represent this fetch_response.
@rtype: {unicode;unicode}
"""
aliases = NamespaceMap()
zero_value_types = []
if self.request is not None:
# Validate the data in the context of the request (the
# same attributes should be present in each, and the
# counts in the response must be no more than the counts
# in the request)
for type_uri in self.data:
if type_uri not in self.request:
raise KeyError(
'Response attribute not present in request: %r' %
(type_uri, ))
for attr_info in self.request.iterAttrs():
# Copy the aliases from the request so that reading
# the response in light of the request is easier
if attr_info.alias is None:
aliases.add(attr_info.type_uri)
else:
aliases.addAlias(attr_info.type_uri, attr_info.alias)
try:
values = self.data[attr_info.type_uri]
except KeyError:
values = []
zero_value_types.append(attr_info)
if (attr_info.count != UNLIMITED_VALUES) and \
(attr_info.count < len(values)):
raise AXError(
'More than the number of requested values were '
'specified for %r' % (attr_info.type_uri, ))
kv_args = self._getExtensionKVArgs(aliases)
# Add the KV args into the response with the args that are
# unique to the fetch_response
ax_args = self._newArgs()
# For each requested attribute, put its type/alias and count
# into the response even if no data were returned.
for attr_info in zero_value_types:
alias = aliases.getAlias(attr_info.type_uri)
kv_args['type.' + alias] = attr_info.type_uri
kv_args['count.' + alias] = '0'
update_url = ((self.request and self.request.update_url) or
self.update_url)
if update_url:
ax_args['update_url'] = update_url
ax_args.update(kv_args)
return ax_args
def parseExtensionArgs(self, ax_args):
"""@see: {Extension.parseExtensionArgs}"""
super(FetchResponse, self).parseExtensionArgs(ax_args)
self.update_url = ax_args.get('update_url')
def fromSuccessResponse(cls, success_response, signed=True):
"""Construct a FetchResponse object from an OpenID library
SuccessResponse object.
@param success_response: A successful id_res response object
@type success_response: openid.consumer.consumer.SuccessResponse
@param signed: Whether non-signed args should be
processsed. If True (the default), only signed arguments
will be processsed.
@type signed: bool
@returns: A FetchResponse containing the data from the OpenID
message, or None if the SuccessResponse did not contain AX
extension data.
@raises AXError: when the AX data cannot be parsed.
"""
self = cls()
ax_args = success_response.extensionResponse(self.ns_uri, signed)
try:
self.parseExtensionArgs(ax_args)
except NotAXMessage as err:
return None
else:
return self
fromSuccessResponse = classmethod(fromSuccessResponse)
class StoreRequest(AXKeyValueMessage):
"""A store request attribute exchange message representation
"""
mode = 'store_request'
def __init__(self, aliases=None):
"""
@param aliases: The namespace aliases to use when making this
store request. Leave as None to use defaults.
"""
super(StoreRequest, self).__init__()
self.aliases = aliases
def getExtensionArgs(self):
"""
@see: L{Extension.getExtensionArgs}
"""
ax_args = self._newArgs()
kv_args = self._getExtensionKVArgs(self.aliases)
ax_args.update(kv_args)
return ax_args
class StoreResponse(AXMessage):
"""An indication that the store request was processed along with
this OpenID transaction.
"""
SUCCESS_MODE = 'store_response_success'
FAILURE_MODE = 'store_response_failure'
def __init__(self, succeeded=True, error_message=None):
AXMessage.__init__(self)
if succeeded and error_message is not None:
raise AXError('An error message may only be included in a '
'failing fetch response')
if succeeded:
self.mode = self.SUCCESS_MODE
else:
self.mode = self.FAILURE_MODE
self.error_message = error_message
def succeeded(self):
"""Was this response a success response?"""
return self.mode == self.SUCCESS_MODE
def getExtensionArgs(self):
"""@see: {Extension.getExtensionArgs}"""
ax_args = self._newArgs()
if not self.succeeded() and self.error_message:
ax_args['error'] = self.error_message
return ax_args
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8718696
python3-openid-3.2.0/openid/extensions/draft/ 0000755 0001750 0001750 00000000000 00000000000 021054 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/extensions/draft/__init__.py 0000644 0001750 0001750 00000000000 00000000000 023153 0 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/extensions/draft/pape2.py 0000644 0001750 0001750 00000022415 00000000000 022441 0 ustar 00rami rami 0000000 0000000 """An implementation of the OpenID Provider Authentication Policy
Extension 1.0
@see: http://openid.net/developers/specs/
@since: 2.1.0
"""
__all__ = [
'Request',
'Response',
'ns_uri',
'AUTH_PHISHING_RESISTANT',
'AUTH_MULTI_FACTOR',
'AUTH_MULTI_FACTOR_PHYSICAL',
]
from openid.extension import Extension
import re
ns_uri = "http://specs.openid.net/extensions/pape/1.0"
AUTH_MULTI_FACTOR_PHYSICAL = \
'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'
AUTH_MULTI_FACTOR = \
'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
AUTH_PHISHING_RESISTANT = \
'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
TIME_VALIDATOR = re.compile('^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$')
class Request(Extension):
"""A Provider Authentication Policy request, sent from a relying
party to a provider
@ivar preferred_auth_policies: The authentication policies that
the relying party prefers
@type preferred_auth_policies: [str]
@ivar max_auth_age: The maximum time, in seconds, that the relying
party wants to allow to have elapsed before the user must
re-authenticate
@type max_auth_age: int or NoneType
"""
ns_alias = 'pape'
def __init__(self, preferred_auth_policies=None, max_auth_age=None):
super(Request, self).__init__()
if not preferred_auth_policies:
preferred_auth_policies = []
self.preferred_auth_policies = preferred_auth_policies
self.max_auth_age = max_auth_age
def __bool__(self):
return bool(self.preferred_auth_policies or
self.max_auth_age is not None)
def addPolicyURI(self, policy_uri):
"""Add an acceptable authentication policy URI to this request
This method is intended to be used by the relying party to add
acceptable authentication types to the request.
@param policy_uri: The identifier for the preferred type of
authentication.
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
"""
if policy_uri not in self.preferred_auth_policies:
self.preferred_auth_policies.append(policy_uri)
def getExtensionArgs(self):
"""@see: C{L{Extension.getExtensionArgs}}
"""
ns_args = {
'preferred_auth_policies': ' '.join(self.preferred_auth_policies)
}
if self.max_auth_age is not None:
ns_args['max_auth_age'] = str(self.max_auth_age)
return ns_args
def fromOpenIDRequest(cls, request):
"""Instantiate a Request object from the arguments in a
C{checkid_*} OpenID message
"""
self = cls()
args = request.message.getArgs(self.ns_uri)
if args == {}:
return None
self.parseExtensionArgs(args)
return self
fromOpenIDRequest = classmethod(fromOpenIDRequest)
def parseExtensionArgs(self, args):
"""Set the state of this request to be that expressed in these
PAPE arguments
@param args: The PAPE arguments without a namespace
@rtype: None
@raises ValueError: When the max_auth_age is not parseable as
an integer
"""
# preferred_auth_policies is a space-separated list of policy URIs
self.preferred_auth_policies = []
policies_str = args.get('preferred_auth_policies')
if policies_str:
if isinstance(policies_str, bytes):
policies_str = str(policies_str, encoding="utf-8")
for uri in policies_str.split(' '):
if uri not in self.preferred_auth_policies:
self.preferred_auth_policies.append(uri)
# max_auth_age is base-10 integer number of seconds
max_auth_age_str = args.get('max_auth_age')
self.max_auth_age = None
if max_auth_age_str:
try:
self.max_auth_age = int(max_auth_age_str)
except ValueError:
pass
def preferredTypes(self, supported_types):
"""Given a list of authentication policy URIs that a provider
supports, this method returns the subsequence of those types
that are preferred by the relying party.
@param supported_types: A sequence of authentication policy
type URIs that are supported by a provider
@returns: The sub-sequence of the supported types that are
preferred by the relying party. This list will be ordered
in the order that the types appear in the supported_types
sequence, and may be empty if the provider does not prefer
any of the supported authentication types.
@returntype: [str]
"""
return list(
filter(self.preferred_auth_policies.__contains__, supported_types))
Request.ns_uri = ns_uri
class Response(Extension):
"""A Provider Authentication Policy response, sent from a provider
to a relying party
"""
ns_alias = 'pape'
def __init__(self,
auth_policies=None,
auth_time=None,
nist_auth_level=None):
super(Response, self).__init__()
if auth_policies:
self.auth_policies = auth_policies
else:
self.auth_policies = []
self.auth_time = auth_time
self.nist_auth_level = nist_auth_level
def addPolicyURI(self, policy_uri):
"""Add a authentication policy to this response
This method is intended to be used by the provider to add a
policy that the provider conformed to when authenticating the user.
@param policy_uri: The identifier for the preferred type of
authentication.
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
"""
if policy_uri not in self.auth_policies:
self.auth_policies.append(policy_uri)
def fromSuccessResponse(cls, success_response):
"""Create a C{L{Response}} object from a successful OpenID
library response
(C{L{openid.consumer.consumer.SuccessResponse}}) response
message
@param success_response: A SuccessResponse from consumer.complete()
@type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
@rtype: Response or None
@returns: A provider authentication policy response from the
data that was supplied with the C{id_res} response or None
if the provider sent no signed PAPE response arguments.
"""
self = cls()
# PAPE requires that the args be signed.
args = success_response.getSignedNS(self.ns_uri)
# Only try to construct a PAPE response if the arguments were
# signed in the OpenID response. If not, return None.
if args is not None:
self.parseExtensionArgs(args)
return self
else:
return None
def parseExtensionArgs(self, args, strict=False):
"""Parse the provider authentication policy arguments into the
internal state of this object
@param args: unqualified provider authentication policy
arguments
@param strict: Whether to raise an exception when bad data is
encountered
@returns: None. The data is parsed into the internal fields of
this object.
"""
policies_str = args.get('auth_policies')
if policies_str and policies_str != 'none':
self.auth_policies = policies_str.split(' ')
nist_level_str = args.get('nist_auth_level')
if nist_level_str:
try:
nist_level = int(nist_level_str)
except ValueError:
if strict:
raise ValueError(
'nist_auth_level must be an integer between '
'zero and four, inclusive')
else:
self.nist_auth_level = None
else:
if 0 <= nist_level < 5:
self.nist_auth_level = nist_level
auth_time = args.get('auth_time')
if auth_time:
if TIME_VALIDATOR.match(auth_time):
self.auth_time = auth_time
elif strict:
raise ValueError("auth_time must be in RFC3339 format")
fromSuccessResponse = classmethod(fromSuccessResponse)
def getExtensionArgs(self):
"""@see: C{L{Extension.getExtensionArgs}}
"""
if len(self.auth_policies) == 0:
ns_args = {
'auth_policies': 'none',
}
else:
ns_args = {
'auth_policies': ' '.join(self.auth_policies),
}
if self.nist_auth_level is not None:
if self.nist_auth_level not in list(range(0, 5)):
raise ValueError('nist_auth_level must be an integer between '
'zero and four, inclusive')
ns_args['nist_auth_level'] = str(self.nist_auth_level)
if self.auth_time is not None:
if not TIME_VALIDATOR.match(self.auth_time):
raise ValueError('auth_time must be in RFC3339 format')
ns_args['auth_time'] = self.auth_time
return ns_args
Response.ns_uri = ns_uri
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586712376.0
python3-openid-3.2.0/openid/extensions/draft/pape5.py 0000644 0001750 0001750 00000040175 00000000000 022447 0 ustar 00rami rami 0000000 0000000 """An implementation of the OpenID Provider Authentication Policy
Extension 1.0, Draft 5
@see: http://openid.net/developers/specs/
@since: 2.1.0
"""
__all__ = [
'Request',
'Response',
'ns_uri',
'AUTH_PHISHING_RESISTANT',
'AUTH_MULTI_FACTOR',
'AUTH_MULTI_FACTOR_PHYSICAL',
'LEVELS_NIST',
'LEVELS_JISA',
]
from openid.extension import Extension
import warnings
import re
ns_uri = "http://specs.openid.net/extensions/pape/1.0"
AUTH_MULTI_FACTOR_PHYSICAL = \
'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'
AUTH_MULTI_FACTOR = \
'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
AUTH_PHISHING_RESISTANT = \
'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
AUTH_NONE = \
'http://schemas.openid.net/pape/policies/2007/06/none'
TIME_VALIDATOR = re.compile(r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$')
LEVELS_NIST = 'http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf'
LEVELS_JISA = 'http://www.jisa.or.jp/spec/auth_level.html'
class PAPEExtension(Extension):
_default_auth_level_aliases = {
'nist': LEVELS_NIST,
'jisa': LEVELS_JISA,
}
def __init__(self):
self.auth_level_aliases = self._default_auth_level_aliases.copy()
def _addAuthLevelAlias(self, auth_level_uri, alias=None):
"""Add an auth level URI alias to this request.
@param auth_level_uri: The auth level URI to send in the
request.
@param alias: The namespace alias to use for this auth level
in this message. May be None if the alias is not
important.
"""
if alias is None:
try:
alias = self._getAlias(auth_level_uri)
except KeyError:
alias = self._generateAlias()
else:
existing_uri = self.auth_level_aliases.get(alias)
if existing_uri is not None and existing_uri != auth_level_uri:
raise KeyError('Attempting to redefine alias %r from %r to %r',
alias, existing_uri, auth_level_uri)
self.auth_level_aliases[alias] = auth_level_uri
def _generateAlias(self):
"""Return an unused auth level alias"""
for i in range(1000):
alias = 'cust%d' % (i, )
if alias not in self.auth_level_aliases:
return alias
raise RuntimeError('Could not find an unused alias (tried 1000!)')
def _getAlias(self, auth_level_uri):
"""Return the alias for the specified auth level URI.
@raises KeyError: if no alias is defined
"""
for (alias, existing_uri) in self.auth_level_aliases.items():
if auth_level_uri == existing_uri:
return alias
raise KeyError(auth_level_uri)
class Request(PAPEExtension):
"""A Provider Authentication Policy request, sent from a relying
party to a provider
@ivar preferred_auth_policies: The authentication policies that
the relying party prefers
@type preferred_auth_policies: [str]
@ivar max_auth_age: The maximum time, in seconds, that the relying
party wants to allow to have elapsed before the user must
re-authenticate
@type max_auth_age: int or NoneType
@ivar preferred_auth_level_types: Ordered list of authentication
level namespace URIs
@type preferred_auth_level_types: [str]
"""
ns_alias = 'pape'
def __init__(self,
preferred_auth_policies=None,
max_auth_age=None,
preferred_auth_level_types=None):
super(Request, self).__init__()
if preferred_auth_policies is None:
preferred_auth_policies = []
self.preferred_auth_policies = preferred_auth_policies
self.max_auth_age = max_auth_age
self.preferred_auth_level_types = []
if preferred_auth_level_types is not None:
for auth_level in preferred_auth_level_types:
self.addAuthLevel(auth_level)
def __bool__(self):
return bool(self.preferred_auth_policies or
self.max_auth_age is not None or
self.preferred_auth_level_types)
def addPolicyURI(self, policy_uri):
"""Add an acceptable authentication policy URI to this request
This method is intended to be used by the relying party to add
acceptable authentication types to the request.
@param policy_uri: The identifier for the preferred type of
authentication.
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-05.html#auth_policies
"""
if policy_uri not in self.preferred_auth_policies:
self.preferred_auth_policies.append(policy_uri)
def addAuthLevel(self, auth_level_uri, alias=None):
self._addAuthLevelAlias(auth_level_uri, alias)
if auth_level_uri not in self.preferred_auth_level_types:
self.preferred_auth_level_types.append(auth_level_uri)
def getExtensionArgs(self):
"""@see: C{L{Extension.getExtensionArgs}}
"""
ns_args = {
'preferred_auth_policies': ' '.join(self.preferred_auth_policies),
}
if self.max_auth_age is not None:
ns_args['max_auth_age'] = str(self.max_auth_age)
if self.preferred_auth_level_types:
preferred_types = []
for auth_level_uri in self.preferred_auth_level_types:
alias = self._getAlias(auth_level_uri)
ns_args['auth_level.ns.%s' % (alias, )] = auth_level_uri
preferred_types.append(alias)
ns_args['preferred_auth_level_types'] = ' '.join(preferred_types)
return ns_args
def fromOpenIDRequest(cls, request):
"""Instantiate a Request object from the arguments in a
C{checkid_*} OpenID message
"""
self = cls()
args = request.message.getArgs(self.ns_uri)
is_openid1 = request.message.isOpenID1()
if args == {}:
return None
self.parseExtensionArgs(args, is_openid1)
return self
fromOpenIDRequest = classmethod(fromOpenIDRequest)
def parseExtensionArgs(self, args, is_openid1, strict=False):
"""Set the state of this request to be that expressed in these
PAPE arguments
@param args: The PAPE arguments without a namespace
@param strict: Whether to raise an exception if the input is
out of spec or otherwise malformed. If strict is false,
malformed input will be ignored.
@param is_openid1: Whether the input should be treated as part
of an OpenID1 request
@rtype: None
@raises ValueError: When the max_auth_age is not parseable as
an integer
"""
# preferred_auth_policies is a space-separated list of policy URIs
self.preferred_auth_policies = []
policies_str = args.get('preferred_auth_policies')
if policies_str:
if isinstance(policies_str, bytes):
policies_str = str(policies_str, encoding="utf-8")
for uri in policies_str.split(' '):
if uri not in self.preferred_auth_policies:
self.preferred_auth_policies.append(uri)
# max_auth_age is base-10 integer number of seconds
max_auth_age_str = args.get('max_auth_age')
self.max_auth_age = None
if max_auth_age_str:
try:
self.max_auth_age = int(max_auth_age_str)
except ValueError:
if strict:
raise
# Parse auth level information
preferred_auth_level_types = args.get('preferred_auth_level_types')
if preferred_auth_level_types:
aliases = preferred_auth_level_types.strip().split()
for alias in aliases:
key = 'auth_level.ns.%s' % (alias, )
try:
uri = args[key]
except KeyError:
if is_openid1:
uri = self._default_auth_level_aliases.get(alias)
else:
uri = None
if uri is None:
if strict:
raise ValueError('preferred auth level %r is not '
'defined in this message' % (alias, ))
else:
self.addAuthLevel(uri, alias)
def preferredTypes(self, supported_types):
"""Given a list of authentication policy URIs that a provider
supports, this method returns the subsequence of those types
that are preferred by the relying party.
@param supported_types: A sequence of authentication policy
type URIs that are supported by a provider
@returns: The sub-sequence of the supported types that are
preferred by the relying party. This list will be ordered
in the order that the types appear in the supported_types
sequence, and may be empty if the provider does not prefer
any of the supported authentication types.
@returntype: [str]
"""
return list(
filter(self.preferred_auth_policies.__contains__, supported_types))
Request.ns_uri = ns_uri
class Response(PAPEExtension):
"""A Provider Authentication Policy response, sent from a provider
to a relying party
@ivar auth_policies: List of authentication policies conformed to
by this OpenID assertion, represented as policy URIs
"""
ns_alias = 'pape'
def __init__(self, auth_policies=None, auth_time=None, auth_levels=None):
super(Response, self).__init__()
if auth_policies:
self.auth_policies = auth_policies
else:
self.auth_policies = []
self.auth_time = auth_time
self.auth_levels = {}
if auth_levels is None:
auth_levels = {}
for uri, level in auth_levels.items():
self.setAuthLevel(uri, level)
def setAuthLevel(self, level_uri, level, alias=None):
"""Set the value for the given auth level type.
@param level: string representation of an authentication level
valid for level_uri
@param alias: An optional namespace alias for the given auth
level URI. May be omitted if the alias is not
significant. The library will use a reasonable default for
widely-used auth level types.
"""
self._addAuthLevelAlias(level_uri, alias)
self.auth_levels[level_uri] = level
def getAuthLevel(self, level_uri):
"""Return the auth level for the specified auth level
identifier
@returns: A string that should map to the auth levels defined
for the auth level type
@raises KeyError: If the auth level type is not present in
this message
"""
return self.auth_levels[level_uri]
def _getNISTAuthLevel(self):
try:
return int(self.getAuthLevel(LEVELS_NIST))
except KeyError:
return None
nist_auth_level = property(
_getNISTAuthLevel,
doc="Backward-compatibility accessor for the NIST auth level")
def addPolicyURI(self, policy_uri):
"""Add a authentication policy to this response
This method is intended to be used by the provider to add a
policy that the provider conformed to when authenticating the user.
@param policy_uri: The identifier for the preferred type of
authentication.
@see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies
"""
if policy_uri == AUTH_NONE:
raise RuntimeError(
'To send no policies, do not set any on the response.')
if policy_uri not in self.auth_policies:
self.auth_policies.append(policy_uri)
def fromSuccessResponse(cls, success_response):
"""Create a C{L{Response}} object from a successful OpenID
library response
(C{L{openid.consumer.consumer.SuccessResponse}}) response
message
@param success_response: A SuccessResponse from consumer.complete()
@type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
@rtype: Response or None
@returns: A provider authentication policy response from the
data that was supplied with the C{id_res} response or None
if the provider sent no signed PAPE response arguments.
"""
self = cls()
# PAPE requires that the args be signed.
args = success_response.getSignedNS(self.ns_uri)
is_openid1 = success_response.isOpenID1()
# Only try to construct a PAPE response if the arguments were
# signed in the OpenID response. If not, return None.
if args is not None:
self.parseExtensionArgs(args, is_openid1)
return self
else:
return None
def parseExtensionArgs(self, args, is_openid1, strict=False):
"""Parse the provider authentication policy arguments into the
internal state of this object
@param args: unqualified provider authentication policy
arguments
@param strict: Whether to raise an exception when bad data is
encountered
@returns: None. The data is parsed into the internal fields of
this object.
"""
policies_str = args.get('auth_policies')
if policies_str:
auth_policies = policies_str.split(' ')
elif strict:
raise ValueError('Missing auth_policies')
else:
auth_policies = []
if (len(auth_policies) > 1 and strict and AUTH_NONE in auth_policies):
raise ValueError('Got some auth policies, as well as the special '
'"none" URI: %r' % (auth_policies, ))
if 'none' in auth_policies:
msg = '"none" used as a policy URI (see PAPE draft < 5)'
if strict:
raise ValueError(msg)
else:
warnings.warn(msg, stacklevel=2)
auth_policies = [
u for u in auth_policies if u not in ['none', AUTH_NONE]
]
self.auth_policies = auth_policies
for (key, val) in args.items():
if key.startswith('auth_level.'):
alias = key[11:]
# skip the already-processed namespace declarations
if alias.startswith('ns.'):
continue
try:
uri = args['auth_level.ns.%s' % (alias, )]
except KeyError:
if is_openid1:
uri = self._default_auth_level_aliases.get(alias)
else:
uri = None
if uri is None:
if strict:
raise ValueError('Undefined auth level alias: %r' %
(alias, ))
else:
self.setAuthLevel(uri, val, alias)
auth_time = args.get('auth_time')
if auth_time:
if TIME_VALIDATOR.match(auth_time):
self.auth_time = auth_time
elif strict:
raise ValueError("auth_time must be in RFC3339 format")
fromSuccessResponse = classmethod(fromSuccessResponse)
def getExtensionArgs(self):
"""@see: C{L{Extension.getExtensionArgs}}
"""
if len(self.auth_policies) == 0:
ns_args = {
'auth_policies': AUTH_NONE,
}
else:
ns_args = {
'auth_policies': ' '.join(self.auth_policies),
}
for level_type, level in self.auth_levels.items():
alias = self._getAlias(level_type)
ns_args['auth_level.ns.%s' % (alias, )] = level_type
ns_args['auth_level.%s' % (alias, )] = str(level)
if self.auth_time is not None:
if not TIME_VALIDATOR.match(self.auth_time):
raise ValueError('auth_time must be in RFC3339 format')
ns_args['auth_time'] = self.auth_time
return ns_args
Response.ns_uri = ns_uri
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/openid/extensions/sreg.py 0000644 0001750 0001750 00000042724 00000000000 021277 0 ustar 00rami rami 0000000 0000000 """Simple registration request and response parsing and object representation
This module contains objects representing simple registration requests
and responses that can be used with both OpenID relying parties and
OpenID providers.
1. The relying party creates a request object and adds it to the
C{L{AuthRequest}} object
before making the C{checkid_} request to the OpenID provider::
auth_request.addExtension(SRegRequest(required=['email']))
2. The OpenID provider extracts the simple registration request from
the OpenID request using C{L{SRegRequest.fromOpenIDRequest}},
gets the user's approval and data, creates a C{L{SRegResponse}}
object and adds it to the C{id_res} response::
sreg_req = SRegRequest.fromOpenIDRequest(checkid_request)
# [ get the user's approval and data, informing the user that
# the fields in sreg_response were requested ]
sreg_resp = SRegResponse.extractResponse(sreg_req, user_data)
sreg_resp.toMessage(openid_response.fields)
3. The relying party uses C{L{SRegResponse.fromSuccessResponse}} to
extract the data from the OpenID response::
sreg_resp = SRegResponse.fromSuccessResponse(success_response)
@since: 2.0
@var sreg_data_fields: The names of the data fields that are listed in
the sreg spec, and a description of them in English
@var sreg_uri: The preferred URI to use for the simple registration
namespace and XRD Type value
"""
from openid.message import registerNamespaceAlias, \
NamespaceAliasRegistrationError
from openid.extension import Extension
import logging
logger = logging.getLogger(__name__)
try:
str #pylint:disable-msg=W0104
except NameError:
# For Python 2.2
str = (str, str) #pylint:disable-msg=W0622
__all__ = [
'SRegRequest',
'SRegResponse',
'data_fields',
'ns_uri',
'ns_uri_1_0',
'ns_uri_1_1',
'supportsSReg',
]
# The data fields that are listed in the sreg spec
data_fields = {
'fullname': 'Full Name',
'nickname': 'Nickname',
'dob': 'Date of Birth',
'email': 'E-mail Address',
'gender': 'Gender',
'postcode': 'Postal Code',
'country': 'Country',
'language': 'Language',
'timezone': 'Time Zone',
}
def checkFieldName(field_name):
"""Check to see that the given value is a valid simple
registration data field name.
@raise ValueError: if the field name is not a valid simple
registration data field name
"""
if field_name not in data_fields:
raise ValueError('%r is not a defined simple registration field' %
(field_name, ))
# URI used in the wild for Yadis documents advertising simple
# registration support
ns_uri_1_0 = 'http://openid.net/sreg/1.0'
# URI in the draft specification for simple registration 1.1
#
ns_uri_1_1 = 'http://openid.net/extensions/sreg/1.1'
# This attribute will always hold the preferred URI to use when adding
# sreg support to an XRDS file or in an OpenID namespace declaration.
ns_uri = ns_uri_1_1
try:
registerNamespaceAlias(ns_uri_1_1, 'sreg')
except NamespaceAliasRegistrationError as e:
logger.exception('registerNamespaceAlias(%r, %r) failed: %s' %
(ns_uri_1_1, 'sreg', str(e), ))
def supportsSReg(endpoint):
"""Does the given endpoint advertise support for simple
registration?
@param endpoint: The endpoint object as returned by OpenID discovery
@type endpoint: openid.consumer.discover.OpenIDEndpoint
@returns: Whether an sreg type was advertised by the endpoint
@rtype: bool
"""
return (endpoint.usesExtension(ns_uri_1_1) or
endpoint.usesExtension(ns_uri_1_0))
class SRegNamespaceError(ValueError):
"""The simple registration namespace was not found and could not
be created using the expected name (there's another extension
using the name 'sreg')
This is not I{illegal}, for OpenID 2, although it probably
indicates a problem, since it's not expected that other extensions
will re-use the alias that is in use for OpenID 1.
If this is an OpenID 1 request, then there is no recourse. This
should not happen unless some code has modified the namespaces for
the message that is being processed.
"""
def getSRegNS(message):
"""Extract the simple registration namespace URI from the given
OpenID message. Handles OpenID 1 and 2, as well as both sreg
namespace URIs found in the wild, as well as missing namespace
definitions (for OpenID 1)
@param message: The OpenID message from which to parse simple
registration fields. This may be a request or response message.
@type message: C{L{openid.message.Message}}
@returns: the sreg namespace URI for the supplied message. The
message may be modified to define a simple registration
namespace.
@rtype: C{str}
@raise ValueError: when using OpenID 1 if the message defines
the 'sreg' alias to be something other than a simple
registration type.
"""
# See if there exists an alias for one of the two defined simple
# registration types.
for sreg_ns_uri in [ns_uri_1_1, ns_uri_1_0]:
alias = message.namespaces.getAlias(sreg_ns_uri)
if alias is not None:
break
else:
# There is no alias for either of the types, so try to add
# one. We default to using the modern value (1.1)
sreg_ns_uri = ns_uri_1_1
try:
message.namespaces.addAlias(ns_uri_1_1, 'sreg')
except KeyError as why:
# An alias for the string 'sreg' already exists, but it's
# defined for something other than simple registration
raise SRegNamespaceError(why)
# we know that sreg_ns_uri defined, because it's defined in the
# else clause of the loop as well, so disable the warning
return sreg_ns_uri #pylint:disable-msg=W0631
class SRegRequest(Extension):
"""An object to hold the state of a simple registration request.
@ivar required: A list of the required fields in this simple
registration request
@type required: [str]
@ivar optional: A list of the optional fields in this simple
registration request
@type optional: [str]
@ivar policy_url: The policy URL that was provided with the request
@type policy_url: str or NoneType
@group Consumer: requestField, requestFields, getExtensionArgs, addToOpenIDRequest
@group Server: fromOpenIDRequest, parseExtensionArgs
"""
ns_alias = 'sreg'
def __init__(self,
required=None,
optional=None,
policy_url=None,
sreg_ns_uri=ns_uri):
"""Initialize an empty simple registration request"""
Extension.__init__(self)
self.required = []
self.optional = []
self.policy_url = policy_url
self.ns_uri = sreg_ns_uri
if required:
self.requestFields(required, required=True, strict=True)
if optional:
self.requestFields(optional, required=False, strict=True)
# Assign getSRegNS to a static method so that it can be
# overridden for testing.
_getSRegNS = staticmethod(getSRegNS)
def fromOpenIDRequest(cls, request):
"""Create a simple registration request that contains the
fields that were requested in the OpenID request with the
given arguments
@param request: The OpenID request
@type request: openid.server.CheckIDRequest
@returns: The newly created simple registration request
@rtype: C{L{SRegRequest}}
"""
self = cls()
# Since we're going to mess with namespace URI mapping, don't
# mutate the object that was passed in.
message = request.message.copy()
self.ns_uri = self._getSRegNS(message)
args = message.getArgs(self.ns_uri)
self.parseExtensionArgs(args)
return self
fromOpenIDRequest = classmethod(fromOpenIDRequest)
def parseExtensionArgs(self, args, strict=False):
"""Parse the unqualified simple registration request
parameters and add them to this object.
This method is essentially the inverse of
C{L{getExtensionArgs}}. This method restores the serialized simple
registration request fields.
If you are extracting arguments from a standard OpenID
checkid_* request, you probably want to use C{L{fromOpenIDRequest}},
which will extract the sreg namespace and arguments from the
OpenID request. This method is intended for cases where the
OpenID server needs more control over how the arguments are
parsed than that method provides.
>>> args = message.getArgs(ns_uri)
>>> request.parseExtensionArgs(args)
@param args: The unqualified simple registration arguments
@type args: {str:str}
@param strict: Whether requests with fields that are not
defined in the simple registration specification should be
tolerated (and ignored)
@type strict: bool
@returns: None; updates this object
"""
for list_name in ['required', 'optional']:
required = (list_name == 'required')
items = args.get(list_name)
if items:
for field_name in items.split(','):
try:
self.requestField(field_name, required, strict)
except ValueError:
if strict:
raise
self.policy_url = args.get('policy_url')
def allRequestedFields(self):
"""A list of all of the simple registration fields that were
requested, whether they were required or optional.
@rtype: [str]
"""
return self.required + self.optional
def wereFieldsRequested(self):
"""Have any simple registration fields been requested?
@rtype: bool
"""
return bool(self.allRequestedFields())
def __contains__(self, field_name):
"""Was this field in the request?"""
return (field_name in self.required or field_name in self.optional)
def requestField(self, field_name, required=False, strict=False):
"""Request the specified field from the OpenID user
@param field_name: the unqualified simple registration field name
@type field_name: str
@param required: whether the given field should be presented
to the user as being a required to successfully complete
the request
@param strict: whether to raise an exception when a field is
added to a request more than once
@raise ValueError: when the field requested is not a simple
registration field or strict is set and the field was
requested more than once
"""
checkFieldName(field_name)
if strict:
if field_name in self.required or field_name in self.optional:
raise ValueError('That field has already been requested')
else:
if field_name in self.required:
return
if field_name in self.optional:
if required:
self.optional.remove(field_name)
else:
return
if required:
self.required.append(field_name)
else:
self.optional.append(field_name)
def requestFields(self, field_names, required=False, strict=False):
"""Add the given list of fields to the request
@param field_names: The simple registration data fields to request
@type field_names: [str]
@param required: Whether these values should be presented to
the user as required
@param strict: whether to raise an exception when a field is
added to a request more than once
@raise ValueError: when a field requested is not a simple
registration field or strict is set and a field was
requested more than once
"""
if isinstance(field_names, str):
raise TypeError('Fields should be passed as a list of '
'strings (not %r)' % (type(field_names), ))
for field_name in field_names:
self.requestField(field_name, required, strict=strict)
def getExtensionArgs(self):
"""Get a dictionary of unqualified simple registration
arguments representing this request.
This method is essentially the inverse of
C{L{parseExtensionArgs}}. This method serializes the simple
registration request fields.
@rtype: {str:str}
"""
args = {}
if self.required:
args['required'] = ','.join(self.required)
if self.optional:
args['optional'] = ','.join(self.optional)
if self.policy_url:
args['policy_url'] = self.policy_url
return args
class SRegResponse(Extension):
"""Represents the data returned in a simple registration response
inside of an OpenID C{id_res} response. This object will be
created by the OpenID server, added to the C{id_res} response
object, and then extracted from the C{id_res} message by the
Consumer.
@ivar data: The simple registration data, keyed by the unqualified
simple registration name of the field (i.e. nickname is keyed
by C{'nickname'})
@ivar ns_uri: The URI under which the simple registration data was
stored in the response message.
@group Server: extractResponse
@group Consumer: fromSuccessResponse
@group Read-only dictionary interface: keys, iterkeys, items, iteritems,
__iter__, get, __getitem__, keys, has_key
"""
ns_alias = 'sreg'
def __init__(self, data=None, sreg_ns_uri=ns_uri):
Extension.__init__(self)
if data is None:
self.data = {}
else:
self.data = data
self.ns_uri = sreg_ns_uri
def extractResponse(cls, request, data):
"""Take a C{L{SRegRequest}} and a dictionary of simple
registration values and create a C{L{SRegResponse}}
object containing that data.
@param request: The simple registration request object
@type request: SRegRequest
@param data: The simple registration data for this
response, as a dictionary from unqualified simple
registration field name to string (unicode) value. For
instance, the nickname should be stored under the key
'nickname'.
@type data: {str:str}
@returns: a simple registration response object
@rtype: SRegResponse
"""
self = cls()
self.ns_uri = request.ns_uri
for field in request.allRequestedFields():
value = data.get(field)
if value is not None:
self.data[field] = value
return self
extractResponse = classmethod(extractResponse)
# Assign getSRegArgs to a static method so that it can be
# overridden for testing
_getSRegNS = staticmethod(getSRegNS)
def fromSuccessResponse(cls, success_response, signed_only=True):
"""Create a C{L{SRegResponse}} object from a successful OpenID
library response
(C{L{openid.consumer.consumer.SuccessResponse}}) response
message
@param success_response: A SuccessResponse from consumer.complete()
@type success_response: C{L{openid.consumer.consumer.SuccessResponse}}
@param signed_only: Whether to process only data that was
signed in the id_res message from the server.
@type signed_only: bool
@rtype: SRegResponse
@returns: A simple registration response containing the data
that was supplied with the C{id_res} response.
"""
self = cls()
self.ns_uri = self._getSRegNS(success_response.message)
if signed_only:
args = success_response.getSignedNS(self.ns_uri)
else:
args = success_response.message.getArgs(self.ns_uri)
if not args:
return None
for field_name in data_fields:
if field_name in args:
self.data[field_name] = args[field_name]
return self
fromSuccessResponse = classmethod(fromSuccessResponse)
def getExtensionArgs(self):
"""Get the fields to put in the simple registration namespace
when adding them to an id_res message.
@see: openid.extension
"""
return self.data
# Read-only dictionary interface
def get(self, field_name, default=None):
"""Like dict.get, except that it checks that the field name is
defined by the simple registration specification"""
checkFieldName(field_name)
return self.data.get(field_name, default)
def items(self):
"""All of the data values in this simple registration response
"""
return list(self.data.items())
def iteritems(self):
return iter(self.data.items())
def keys(self):
return list(self.data.keys())
def iterkeys(self):
return iter(self.data.keys())
def has_key(self, key):
return key in self
def __contains__(self, field_name):
checkFieldName(field_name)
return field_name in self.data
def __iter__(self):
return iter(self.data)
def __getitem__(self, field_name):
checkFieldName(field_name)
return self.data[field_name]
def __bool__(self):
return bool(self.data)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/fetchers.py 0000644 0001750 0001750 00000037011 00000000000 017734 0 ustar 00rami rami 0000000 0000000 # -*- test-case-name: openid.test.test_fetchers -*-
"""
This module contains the HTTP fetcher interface and several implementations.
"""
__all__ = [
'fetch', 'getDefaultFetcher', 'setDefaultFetcher', 'HTTPResponse',
'HTTPFetcher', 'createHTTPFetcher', 'HTTPFetchingError', 'HTTPError'
]
import urllib.request
import urllib.error
import urllib.parse
import http.client
import time
import io
import sys
import contextlib
import openid
import openid.urinorm
# Try to import httplib2 for caching support
# http://bitworking.org/projects/httplib2/
try:
import httplib2
except ImportError:
# httplib2 not available
httplib2 = None
# try to import pycurl, which will let us use CurlHTTPFetcher
try:
import pycurl
except ImportError:
pycurl = None
USER_AGENT = "python-openid/%s (%s)" % (openid.__version__, sys.platform)
MAX_RESPONSE_KB = 1024
def fetch(url, body=None, headers=None):
"""Invoke the fetch method on the default fetcher. Most users
should need only this method.
@raises Exception: any exceptions that may be raised by the default fetcher
"""
fetcher = getDefaultFetcher()
return fetcher.fetch(url, body, headers)
def createHTTPFetcher():
"""Create a default HTTP fetcher instance
prefers Curl to urllib2."""
if pycurl is None:
fetcher = Urllib2Fetcher()
else:
fetcher = CurlHTTPFetcher()
return fetcher
# Contains the currently set HTTP fetcher. If it is set to None, the
# library will call createHTTPFetcher() to set it. Do not access this
# variable outside of this module.
_default_fetcher = None
def getDefaultFetcher():
"""Return the default fetcher instance
if no fetcher has been set, it will create a default fetcher.
@return: the default fetcher
@rtype: HTTPFetcher
"""
global _default_fetcher
if _default_fetcher is None:
setDefaultFetcher(createHTTPFetcher())
return _default_fetcher
def setDefaultFetcher(fetcher, wrap_exceptions=True):
"""Set the default fetcher
@param fetcher: The fetcher to use as the default HTTP fetcher
@type fetcher: HTTPFetcher
@param wrap_exceptions: Whether to wrap exceptions thrown by the
fetcher wil HTTPFetchingError so that they may be caught
easier. By default, exceptions will be wrapped. In general,
unwrapped fetchers are useful for debugging of fetching errors
or if your fetcher raises well-known exceptions that you would
like to catch.
@type wrap_exceptions: bool
"""
global _default_fetcher
if fetcher is None or not wrap_exceptions:
_default_fetcher = fetcher
else:
_default_fetcher = ExceptionWrappingFetcher(fetcher)
def usingCurl():
"""Whether the currently set HTTP fetcher is a Curl HTTP fetcher."""
fetcher = getDefaultFetcher()
if isinstance(fetcher, ExceptionWrappingFetcher):
fetcher = fetcher.fetcher
return isinstance(fetcher, CurlHTTPFetcher)
class HTTPResponse(object):
"""XXX document attributes"""
headers = None
status = None
body = None
final_url = None
def __init__(self, final_url=None, status=None, headers=None, body=None):
self.final_url = final_url
self.status = status
self.headers = headers
self.body = body
def __repr__(self):
return "<%s status %s for %s>" % (self.__class__.__name__, self.status,
self.final_url)
class HTTPFetcher(object):
"""
This class is the interface for openid HTTP fetchers. This
interface is only important if you need to write a new fetcher for
some reason.
"""
def fetch(self, url, body=None, headers=None):
"""
This performs an HTTP POST or GET, following redirects along
the way. If a body is specified, then the request will be a
POST. Otherwise, it will be a GET.
@param headers: HTTP headers to include with the request
@type headers: {str:str}
@return: An object representing the server's HTTP response. If
there are network or protocol errors, an exception will be
raised. HTTP error responses, like 404 or 500, do not
cause exceptions.
@rtype: L{HTTPResponse}
@raise Exception: Different implementations will raise
different errors based on the underlying HTTP library.
"""
raise NotImplementedError
def _allowedURL(url):
parsed = urllib.parse.urlparse(url)
# scheme is the first item in the tuple
return parsed[0] in ('http', 'https')
class HTTPFetchingError(Exception):
"""Exception that is wrapped around all exceptions that are raised
by the underlying fetcher when using the ExceptionWrappingFetcher
@ivar why: The exception that caused this exception
"""
def __init__(self, why=None):
Exception.__init__(self, why)
self.why = why
class ExceptionWrappingFetcher(HTTPFetcher):
"""Fetcher that wraps another fetcher, causing all exceptions
@cvar uncaught_exceptions: Exceptions that should be exposed to the
user if they are raised by the fetch call
"""
uncaught_exceptions = (SystemExit, KeyboardInterrupt, MemoryError)
def __init__(self, fetcher):
self.fetcher = fetcher
def fetch(self, *args, **kwargs):
try:
return self.fetcher.fetch(*args, **kwargs)
except self.uncaught_exceptions:
raise
except:
exc_cls, exc_inst = sys.exc_info()[:2]
if exc_inst is None:
# string exceptions
exc_inst = exc_cls
raise HTTPFetchingError(why=exc_inst)
class Urllib2Fetcher(HTTPFetcher):
"""An C{L{HTTPFetcher}} that uses urllib2.
"""
# Parameterized for the benefit of testing frameworks, see
# http://trac.openidenabled.com/trac/ticket/85
urlopen = staticmethod(urllib.request.urlopen)
def fetch(self, url, body=None, headers=None):
if not _allowedURL(url):
raise ValueError('Bad URL scheme: %r' % (url, ))
if headers is None:
headers = {}
headers.setdefault('User-Agent', "%s Python-urllib/%s" %
(USER_AGENT, urllib.request.__version__))
if isinstance(body, str):
body = bytes(body, encoding="utf-8")
req = urllib.request.Request(url, data=body, headers=headers)
url_resource = None
try:
url_resource = self.urlopen(req)
with contextlib.closing(url_resource):
return self._makeResponse(url_resource)
except urllib.error.HTTPError as why:
with contextlib.closing(why):
resp = self._makeResponse(why)
return resp
except (urllib.error.URLError, http.client.BadStatusLine) as why:
raise
except Exception as why:
raise AssertionError(why)
def _makeResponse(self, urllib2_response):
'''
Construct an HTTPResponse from the the urllib response. Attempt to
decode the response body from bytes to str if the necessary information
is available.
'''
resp = HTTPResponse()
resp.body = urllib2_response.read(MAX_RESPONSE_KB * 1024)
resp.final_url = urllib2_response.geturl()
resp.headers = self._lowerCaseKeys(
dict(list(urllib2_response.info().items())))
if hasattr(urllib2_response, 'code'):
resp.status = urllib2_response.code
else:
resp.status = 200
_, extra_dict = self._parseHeaderValue(
resp.headers.get("content-type", ""))
# Try to decode the response body to a string, if there's a
# charset known; fall back to ISO-8859-1 otherwise, since that's
# what's suggested in HTTP/1.1
charset = extra_dict.get('charset', 'latin1')
try:
resp.body = resp.body.decode(charset)
except Exception:
pass
return resp
def _lowerCaseKeys(self, headers_dict):
new_dict = {}
for k, v in headers_dict.items():
new_dict[k.lower()] = v
return new_dict
def _parseHeaderValue(self, header_value):
"""
Parse out a complex header value (such as Content-Type, with a value
like "text/html; charset=utf-8") into a main value and a dictionary of
extra information (in this case, 'text/html' and {'charset': 'utf8'}).
"""
values = header_value.split(';', 1)
if len(values) == 1:
# There's no extra info -- return the main value and an empty dict
return values[0], {}
main_value, extra_values = values[0], values[1].split(';')
extra_dict = {}
for value_string in extra_values:
try:
key, value = value_string.split('=', 1)
extra_dict[key.strip()] = value.strip()
except ValueError:
# Can't unpack it -- must be malformed. Ignore
pass
return main_value, extra_dict
class HTTPError(HTTPFetchingError):
"""
This exception is raised by the C{L{CurlHTTPFetcher}} when it
encounters an exceptional situation fetching a URL.
"""
pass
# XXX: define what we mean by paranoid, and make sure it is.
class CurlHTTPFetcher(HTTPFetcher):
"""
An C{L{HTTPFetcher}} that uses pycurl for fetching.
See U{http://pycurl.sourceforge.net/}.
"""
ALLOWED_TIME = 20 # seconds
def __init__(self):
HTTPFetcher.__init__(self)
if pycurl is None:
raise RuntimeError('Cannot find pycurl library')
def _parseHeaders(self, header_file):
header_file.seek(0)
# Remove all non "name: value" header lines from the input
lines = [line.decode().strip() for line in header_file if b':' in line]
headers = {}
for line in lines:
try:
name, value = line.split(':', 1)
except ValueError:
raise HTTPError("Malformed HTTP header line in response: %r" %
(line, ))
value = value.strip()
# HTTP headers are case-insensitive
name = name.lower()
headers[name] = value
return headers
def _checkURL(self, url):
# XXX: document that this can be overridden to match desired policy
# XXX: make sure url is well-formed and routeable
return _allowedURL(url)
def fetch(self, url, body=None, headers=None):
stop = int(time.time()) + self.ALLOWED_TIME
off = self.ALLOWED_TIME
if headers is None:
headers = {}
headers.setdefault('User-Agent',
"%s %s" % (USER_AGENT, pycurl.version, ))
header_list = []
if headers is not None:
for header_name, header_value in headers.items():
header = '%s: %s' % (header_name, header_value)
header_list.append(header.encode())
c = pycurl.Curl()
try:
c.setopt(pycurl.NOSIGNAL, 1)
if header_list:
c.setopt(pycurl.HTTPHEADER, header_list)
# Presence of a body indicates that we should do a POST
if body is not None:
c.setopt(pycurl.POST, 1)
c.setopt(pycurl.POSTFIELDS, body)
while off > 0:
if not self._checkURL(url):
raise HTTPError("Fetching URL not allowed: %r" % (url, ))
data = io.BytesIO()
def write_data(chunk):
if data.tell() > (1024 * MAX_RESPONSE_KB):
return 0
else:
return data.write(chunk)
response_header_data = io.BytesIO()
c.setopt(pycurl.WRITEFUNCTION, write_data)
c.setopt(pycurl.HEADERFUNCTION, response_header_data.write)
c.setopt(pycurl.TIMEOUT, off)
c.setopt(pycurl.URL, openid.urinorm.urinorm(url))
c.perform()
response_headers = self._parseHeaders(response_header_data)
code = c.getinfo(pycurl.RESPONSE_CODE)
if code in [301, 302, 303, 307]:
url = response_headers.get('location')
if url is None:
raise HTTPError(
'Redirect (%s) returned without a location' % code)
# Redirects are always GETs
c.setopt(pycurl.POST, 0)
# There is no way to reset POSTFIELDS to empty and
# reuse the connection, but we only use it once.
else:
resp = HTTPResponse()
resp.headers = response_headers
resp.status = code
resp.final_url = url
resp.body = data.getvalue().decode()
return resp
off = stop - int(time.time())
raise HTTPError("Timed out fetching: %r" % (url, ))
finally:
c.close()
class HTTPLib2Fetcher(HTTPFetcher):
"""A fetcher that uses C{httplib2} for performing HTTP
requests. This implementation supports HTTP caching.
@see: http://bitworking.org/projects/httplib2/
"""
def __init__(self, cache=None):
"""@param cache: An object suitable for use as an C{httplib2}
cache. If a string is passed, it is assumed to be a
directory name.
"""
if httplib2 is None:
raise RuntimeError('Cannot find httplib2 library. '
'See http://bitworking.org/projects/httplib2/')
super(HTTPLib2Fetcher, self).__init__()
# An instance of the httplib2 object that performs HTTP requests
self.httplib2 = httplib2.Http(cache)
# We want httplib2 to raise exceptions for errors, just like
# the other fetchers.
self.httplib2.force_exception_to_status_code = False
def fetch(self, url, body=None, headers=None):
"""Perform an HTTP request
@raises Exception: Any exception that can be raised by httplib2
@see: C{L{HTTPFetcher.fetch}}
"""
if body:
method = 'POST'
else:
method = 'GET'
if headers is None:
headers = {}
# httplib2 doesn't check to make sure that the URL's scheme is
# 'http' so we do it here.
if not (url.startswith('http://') or url.startswith('https://')):
raise ValueError('URL is not a HTTP URL: %r' % (url, ))
httplib2_response, content = self.httplib2.request(
url, method, body=body, headers=headers)
# Translate the httplib2 response to our HTTP response abstraction
# When a 400 is returned, there is no "content-location"
# header set. This seems like a bug to me. I can't think of a
# case where we really care about the final URL when it is an
# error response, but being careful about it can't hurt.
try:
final_url = httplib2_response['content-location']
except KeyError:
# We're assuming that no redirects occurred
assert not httplib2_response.previous
# And this should never happen for a successful response
assert httplib2_response.status != 200
final_url = url
return HTTPResponse(
body=content.decode(), # TODO Don't assume ASCII
final_url=final_url,
headers=dict(list(httplib2_response.items())),
status=httplib2_response.status, )
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/openid/kvform.py 0000644 0001750 0001750 00000006404 00000000000 017437 0 ustar 00rami rami 0000000 0000000 import logging
logger = logging.getLogger(__name__)
__all__ = ['seqToKV', 'kvToSeq', 'dictToKV', 'kvToDict']
class KVFormError(ValueError):
pass
def seqToKV(seq, strict=False):
"""Represent a sequence of pairs of strings as newline-terminated
key:value pairs. The pairs are generated in the order given.
@param seq: The pairs
@type seq: [(str, (unicode|str))]
@return: A string representation of the sequence
@rtype: bytes
"""
def err(msg):
formatted = 'seqToKV warning: %s: %r' % (msg, seq)
if strict:
raise KVFormError(formatted)
else:
logger.warning(formatted)
lines = []
for k, v in seq:
if isinstance(k, bytes):
k = k.decode('utf-8')
elif not isinstance(k, str):
err('Converting key to string: %r' % k)
k = str(k)
if '\n' in k:
raise KVFormError(
'Invalid input for seqToKV: key contains newline: %r' % (k, ))
if ':' in k:
raise KVFormError(
'Invalid input for seqToKV: key contains colon: %r' % (k, ))
if k.strip() != k:
err('Key has whitespace at beginning or end: %r' % (k, ))
if isinstance(v, bytes):
v = v.decode('utf-8')
elif not isinstance(v, str):
err('Converting value to string: %r' % (v, ))
v = str(v)
if '\n' in v:
raise KVFormError(
'Invalid input for seqToKV: value contains newline: %r' %
(v, ))
if v.strip() != v:
err('Value has whitespace at beginning or end: %r' % (v, ))
lines.append(k + ':' + v + '\n')
return ''.join(lines).encode('utf-8')
def kvToSeq(data, strict=False):
"""
After one parse, seqToKV and kvToSeq are inverses, with no warnings::
seq = kvToSeq(s)
seqToKV(kvToSeq(seq)) == seq
@return str
"""
def err(msg):
formatted = 'kvToSeq warning: %s: %r' % (msg, data)
if strict:
raise KVFormError(formatted)
else:
logger.warning(formatted)
if isinstance(data, bytes):
data = data.decode("utf-8")
lines = data.split('\n')
if lines[-1]:
err('Does not end in a newline')
else:
del lines[-1]
pairs = []
line_num = 0
for line in lines:
line_num += 1
# Ignore blank lines
if not line.strip():
continue
pair = line.split(':', 1)
if len(pair) == 2:
k, v = pair
k_s = k.strip()
if k_s != k:
fmt = ('In line %d, ignoring leading or trailing '
'whitespace in key %r')
err(fmt % (line_num, k))
if not k_s:
err('In line %d, got empty key' % (line_num, ))
v_s = v.strip()
if v_s != v:
fmt = ('In line %d, ignoring leading or trailing '
'whitespace in value %r')
err(fmt % (line_num, v))
pairs.append((k_s, v_s))
else:
err('Line %d does not contain a colon' % line_num)
return pairs
def dictToKV(d):
return seqToKV(sorted(d.items()))
def kvToDict(s):
return dict(kvToSeq(s))
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/message.py 0000644 0001750 0001750 00000054230 00000000000 017557 0 ustar 00rami rami 0000000 0000000 """Extension argument processing code
"""
__all__ = [
'Message', 'NamespaceMap', 'no_default', 'registerNamespaceAlias',
'OPENID_NS', 'BARE_NS', 'OPENID1_NS', 'OPENID2_NS', 'SREG_URI',
'IDENTIFIER_SELECT'
]
import copy
import warnings
import urllib.request
import urllib.error
from openid import oidutil
from openid import kvform
try:
ElementTree = oidutil.importElementTree()
except ImportError:
# No elementtree found, so give up, but don't fail to import,
# since we have fallbacks.
ElementTree = None
# This doesn't REALLY belong here, but where is better?
IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select'
# URI for Simple Registration extension, the only commonly deployed
# OpenID 1.x extension, and so a special case
SREG_URI = 'http://openid.net/sreg/1.0'
# The OpenID 1.X namespace URI
OPENID1_NS = 'http://openid.net/signon/1.0'
THE_OTHER_OPENID1_NS = 'http://openid.net/signon/1.1'
OPENID1_NAMESPACES = OPENID1_NS, THE_OTHER_OPENID1_NS
# The OpenID 2.0 namespace URI
OPENID2_NS = 'http://specs.openid.net/auth/2.0'
# The namespace consisting of pairs with keys that are prefixed with
# "openid." but not in another namespace.
NULL_NAMESPACE = oidutil.Symbol('Null namespace')
# The null namespace, when it is an allowed OpenID namespace
OPENID_NS = oidutil.Symbol('OpenID namespace')
# The top-level namespace, excluding all pairs with keys that start
# with "openid."
BARE_NS = oidutil.Symbol('Bare namespace')
# Limit, in bytes, of identity provider and return_to URLs, including
# response payload. See OpenID 1.1 specification, Appendix D.
OPENID1_URL_LIMIT = 2047
# All OpenID protocol fields. Used to check namespace aliases.
OPENID_PROTOCOL_FIELDS = [
'ns',
'mode',
'error',
'return_to',
'contact',
'reference',
'signed',
'assoc_type',
'session_type',
'dh_modulus',
'dh_gen',
'dh_consumer_public',
'claimed_id',
'identity',
'realm',
'invalidate_handle',
'op_endpoint',
'response_nonce',
'sig',
'assoc_handle',
'trust_root',
'openid',
]
class UndefinedOpenIDNamespace(ValueError):
"""Raised if the generic OpenID namespace is accessed when there
is no OpenID namespace set for this message."""
class InvalidOpenIDNamespace(ValueError):
"""Raised if openid.ns is not a recognized value.
For recognized values, see L{Message.allowed_openid_namespaces}
"""
def __str__(self):
s = "Invalid OpenID Namespace"
if self.args:
s += " %r" % (self.args[0], )
return s
# Sentinel used for Message implementation to indicate that getArg
# should raise an exception instead of returning a default.
no_default = object()
# Global namespace / alias registration map. See
# registerNamespaceAlias.
registered_aliases = {}
class NamespaceAliasRegistrationError(Exception):
"""
Raised when an alias or namespace URI has already been registered.
"""
pass
def registerNamespaceAlias(namespace_uri, alias):
"""
Registers a (namespace URI, alias) mapping in a global namespace
alias map. Raises NamespaceAliasRegistrationError if either the
namespace URI or alias has already been registered with a
different value. This function is required if you want to use a
namespace with an OpenID 1 message.
"""
global registered_aliases
if registered_aliases.get(alias) == namespace_uri:
return
if namespace_uri in list(registered_aliases.values()):
raise NamespaceAliasRegistrationError(
'Namespace uri %r already registered' % (namespace_uri, ))
if alias in registered_aliases:
raise NamespaceAliasRegistrationError('Alias %r already registered' %
(alias, ))
registered_aliases[alias] = namespace_uri
class Message(object):
"""
In the implementation of this object, None represents the global
namespace as well as a namespace with no key.
@cvar namespaces: A dictionary specifying specific
namespace-URI to alias mappings that should be used when
generating namespace aliases.
@ivar ns_args: two-level dictionary of the values in this message,
grouped by namespace URI. The first level is the namespace
URI.
"""
allowed_openid_namespaces = [OPENID1_NS, THE_OTHER_OPENID1_NS, OPENID2_NS]
def __init__(self, openid_namespace=None):
"""Create an empty Message.
@raises InvalidOpenIDNamespace: if openid_namespace is not in
L{Message.allowed_openid_namespaces}
"""
self.args = {}
self.namespaces = NamespaceMap()
if openid_namespace is None:
self._openid_ns_uri = None
else:
implicit = openid_namespace in OPENID1_NAMESPACES
self.setOpenIDNamespace(openid_namespace, implicit)
@classmethod
def fromPostArgs(cls, args):
"""Construct a Message containing a set of POST arguments.
"""
self = cls()
# Partition into "openid." args and bare args
openid_args = {}
for key, value in args.items():
if isinstance(value, list):
raise TypeError("query dict must have one value for each key, "
"not lists of values. Query is %r" % (args, ))
try:
prefix, rest = key.split('.', 1)
except ValueError:
prefix = None
if prefix != 'openid':
self.args[(BARE_NS, key)] = value
else:
openid_args[rest] = value
self._fromOpenIDArgs(openid_args)
return self
@classmethod
def fromOpenIDArgs(cls, openid_args):
"""Construct a Message from a parsed KVForm message.
@raises InvalidOpenIDNamespace: if openid.ns is not in
L{Message.allowed_openid_namespaces}
"""
self = cls()
self._fromOpenIDArgs(openid_args)
return self
def _fromOpenIDArgs(self, openid_args):
ns_args = []
# Resolve namespaces
for rest, value in openid_args.items():
try:
ns_alias, ns_key = rest.split('.', 1)
except ValueError:
ns_alias = NULL_NAMESPACE
ns_key = rest
if ns_alias == 'ns':
self.namespaces.addAlias(value, ns_key)
elif ns_alias == NULL_NAMESPACE and ns_key == 'ns':
# null namespace
self.setOpenIDNamespace(value, False)
else:
ns_args.append((ns_alias, ns_key, value))
# Implicitly set an OpenID namespace definition (OpenID 1)
if not self.getOpenIDNamespace():
self.setOpenIDNamespace(OPENID1_NS, True)
# Actually put the pairs into the appropriate namespaces
for (ns_alias, ns_key, value) in ns_args:
ns_uri = self.namespaces.getNamespaceURI(ns_alias)
if ns_uri is None:
# we found a namespaced arg without a namespace URI defined
ns_uri = self._getDefaultNamespace(ns_alias)
if ns_uri is None:
ns_uri = self.getOpenIDNamespace()
ns_key = '%s.%s' % (ns_alias, ns_key)
else:
self.namespaces.addAlias(ns_uri, ns_alias, implicit=True)
self.setArg(ns_uri, ns_key, value)
def _getDefaultNamespace(self, mystery_alias):
"""OpenID 1 compatibility: look for a default namespace URI to
use for this alias."""
global registered_aliases
# Only try to map an alias to a default if it's an
# OpenID 1.x message.
if self.isOpenID1():
return registered_aliases.get(mystery_alias)
else:
return None
def setOpenIDNamespace(self, openid_ns_uri, implicit):
"""Set the OpenID namespace URI used in this message.
@raises InvalidOpenIDNamespace: if the namespace is not in
L{Message.allowed_openid_namespaces}
"""
if isinstance(openid_ns_uri, bytes):
openid_ns_uri = str(openid_ns_uri, encoding="utf-8")
if openid_ns_uri not in self.allowed_openid_namespaces:
raise InvalidOpenIDNamespace(openid_ns_uri)
self.namespaces.addAlias(openid_ns_uri, NULL_NAMESPACE, implicit)
self._openid_ns_uri = openid_ns_uri
def getOpenIDNamespace(self):
return self._openid_ns_uri
def isOpenID1(self):
return self.getOpenIDNamespace() in OPENID1_NAMESPACES
def isOpenID2(self):
return self.getOpenIDNamespace() == OPENID2_NS
def fromKVForm(cls, kvform_string):
"""Create a Message from a KVForm string"""
return cls.fromOpenIDArgs(kvform.kvToDict(kvform_string))
fromKVForm = classmethod(fromKVForm)
def copy(self):
return copy.deepcopy(self)
def toPostArgs(self):
"""
Return all arguments with openid. in front of namespaced arguments.
@return bytes
"""
args = {}
# Add namespace definitions to the output
for ns_uri, alias in self.namespaces.items():
if self.namespaces.isImplicit(ns_uri):
continue
if alias == NULL_NAMESPACE:
ns_key = 'openid.ns'
else:
ns_key = 'openid.ns.' + alias
args[ns_key] = oidutil.toUnicode(ns_uri)
for (ns_uri, ns_key), value in self.args.items():
key = self.getKey(ns_uri, ns_key)
# Ensure the resulting value is an UTF-8 encoded *bytestring*.
args[key] = oidutil.toUnicode(value)
return args
def toArgs(self):
"""Return all namespaced arguments, failing if any
non-namespaced arguments exist."""
# FIXME - undocumented exception
post_args = self.toPostArgs()
kvargs = {}
for k, v in post_args.items():
if not k.startswith('openid.'):
raise ValueError(
'This message can only be encoded as a POST, because it '
'contains arguments that are not prefixed with "openid."')
else:
kvargs[k[7:]] = v
return kvargs
def toFormMarkup(self,
action_url,
form_tag_attrs=None,
submit_text="Continue"):
"""Generate HTML form markup that contains the values in this
message, to be HTTP POSTed as x-www-form-urlencoded UTF-8.
@param action_url: The URL to which the form will be POSTed
@type action_url: str
@param form_tag_attrs: Dictionary of attributes to be added to
the form tag. 'accept-charset' and 'enctype' have defaults
that can be overridden. If a value is supplied for
'action' or 'method', it will be replaced.
@type form_tag_attrs: {unicode: unicode}
@param submit_text: The text that will appear on the submit
button for this form.
@type submit_text: unicode
@returns: A string containing (X)HTML markup for a form that
encodes the values in this Message object.
@rtype: str
"""
if ElementTree is None:
raise RuntimeError('This function requires ElementTree.')
assert action_url is not None
form = ElementTree.Element('form')
if form_tag_attrs:
for name, attr in form_tag_attrs.items():
form.attrib[name] = attr
form.attrib['action'] = oidutil.toUnicode(action_url)
form.attrib['method'] = 'post'
form.attrib['accept-charset'] = 'UTF-8'
form.attrib['enctype'] = 'application/x-www-form-urlencoded'
for name, value in self.toPostArgs().items():
attrs = {
'type': 'hidden',
'name': oidutil.toUnicode(name),
'value': oidutil.toUnicode(value)
}
form.append(ElementTree.Element('input', attrs))
submit = ElementTree.Element(
'input',
{'type': 'submit',
'value': oidutil.toUnicode(submit_text)})
form.append(submit)
return str(ElementTree.tostring(form, encoding='utf-8'),
encoding="utf-8")
def toURL(self, base_url):
"""Generate a GET URL with the parameters in this message
attached as query parameters."""
return oidutil.appendArgs(base_url, self.toPostArgs())
def toKVForm(self):
"""Generate a KVForm string that contains the parameters in
this message. This will fail if the message contains arguments
outside of the 'openid.' prefix.
"""
return kvform.dictToKV(self.toArgs())
def toURLEncoded(self):
"""Generate an x-www-urlencoded string"""
args = sorted(self.toPostArgs().items())
return urllib.parse.urlencode(args)
def _fixNS(self, namespace):
"""Convert an input value into the internally used values of
this object
@param namespace: The string or constant to convert
@type namespace: str or unicode or BARE_NS or OPENID_NS
"""
if isinstance(namespace, bytes):
namespace = str(namespace, encoding="utf-8")
if namespace == OPENID_NS:
if self._openid_ns_uri is None:
raise UndefinedOpenIDNamespace('OpenID namespace not set')
else:
namespace = self._openid_ns_uri
if namespace != BARE_NS and not isinstance(namespace, str):
raise TypeError(
"Namespace must be BARE_NS, OPENID_NS or a string. got %r" %
(namespace, ))
if namespace != BARE_NS and ':' not in namespace:
fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r'
warnings.warn(fmt % (namespace, ), DeprecationWarning)
if namespace == 'sreg':
fmt = 'Using %r instead of "sreg" as namespace'
warnings.warn(
fmt % (SREG_URI, ),
DeprecationWarning, )
return SREG_URI
return namespace
def hasKey(self, namespace, ns_key):
namespace = self._fixNS(namespace)
return (namespace, ns_key) in self.args
def getKey(self, namespace, ns_key):
"""Get the key for a particular namespaced argument"""
namespace = self._fixNS(namespace)
if namespace == BARE_NS:
return ns_key
ns_alias = self.namespaces.getAlias(namespace)
# No alias is defined, so no key can exist
if ns_alias is None:
return None
if ns_alias == NULL_NAMESPACE:
tail = ns_key
else:
tail = '%s.%s' % (ns_alias, ns_key)
return 'openid.' + tail
def getArg(self, namespace, key, default=None):
"""Get a value for a namespaced key.
@param namespace: The namespace in the message for this key
@type namespace: str
@param key: The key to get within this namespace
@type key: str
@param default: The value to use if this key is absent from
this message. Using the special value
openid.message.no_default will result in this method
raising a KeyError instead of returning the default.
@rtype: str or the type of default
@raises KeyError: if default is no_default
@raises UndefinedOpenIDNamespace: if the message has not yet
had an OpenID namespace set
"""
namespace = self._fixNS(namespace)
args_key = (namespace, key)
try:
return self.args[args_key]
except KeyError:
if default is no_default:
raise KeyError((namespace, key))
else:
return default
def getArgs(self, namespace):
"""Get the arguments that are defined for this namespace URI
@returns: mapping from namespaced keys to values
@returntype: dict of {str:bytes}
"""
namespace = self._fixNS(namespace)
args = []
for ((pair_ns, ns_key), value) in self.args.items():
if pair_ns == namespace:
if isinstance(ns_key, bytes):
k = str(ns_key, encoding="utf-8")
else:
k = ns_key
if isinstance(value, bytes):
v = str(value, encoding="utf-8")
else:
v = value
args.append((k, v))
return dict(args)
def updateArgs(self, namespace, updates):
"""Set multiple key/value pairs in one call
@param updates: The values to set
@type updates: {unicode:unicode}
"""
namespace = self._fixNS(namespace)
for k, v in updates.items():
self.setArg(namespace, k, v)
def setArg(self, namespace, key, value):
"""Set a single argument in this namespace"""
assert key is not None
assert value is not None
namespace = self._fixNS(namespace)
# try to ensure that internally it's consistent, at least: str -> str
if isinstance(value, bytes):
value = str(value, encoding="utf-8")
self.args[(namespace, key)] = value
if not (namespace is BARE_NS):
self.namespaces.add(namespace)
def delArg(self, namespace, key):
namespace = self._fixNS(namespace)
del self.args[(namespace, key)]
def __repr__(self):
return "<%s.%s %r>" % (self.__class__.__module__,
self.__class__.__name__, self.args)
def __eq__(self, other):
return self.args == other.args
def __ne__(self, other):
return not (self == other)
def getAliasedArg(self, aliased_key, default=None):
if aliased_key == 'ns':
return self.getOpenIDNamespace()
if aliased_key.startswith('ns.'):
uri = self.namespaces.getNamespaceURI(aliased_key[3:])
if uri is None:
if default == no_default:
raise KeyError
else:
return default
else:
return uri
try:
alias, key = aliased_key.split('.', 1)
except ValueError:
# need more than x values to unpack
ns = None
else:
ns = self.namespaces.getNamespaceURI(alias)
if ns is None:
key = aliased_key
ns = self.getOpenIDNamespace()
return self.getArg(ns, key, default)
class NamespaceMap(object):
"""Maintains a bijective map between namespace uris and aliases.
"""
def __init__(self):
self.alias_to_namespace = {}
self.namespace_to_alias = {}
self.implicit_namespaces = []
def getAlias(self, namespace_uri):
return self.namespace_to_alias.get(namespace_uri)
def getNamespaceURI(self, alias):
return self.alias_to_namespace.get(alias)
def iterNamespaceURIs(self):
"""Return an iterator over the namespace URIs"""
return iter(self.namespace_to_alias)
def iterAliases(self):
"""Return an iterator over the aliases"""
return iter(self.alias_to_namespace)
def items(self):
"""Iterate over the mapping
@returns: iterator of (namespace_uri, alias)
"""
return self.namespace_to_alias.items()
def addAlias(self, namespace_uri, desired_alias, implicit=False):
"""Add an alias from this namespace URI to the desired alias
"""
if isinstance(namespace_uri, bytes):
namespace_uri = str(namespace_uri, encoding="utf-8")
# Check that desired_alias is not an openid protocol field as
# per the spec.
assert desired_alias not in OPENID_PROTOCOL_FIELDS, \
"%r is not an allowed namespace alias" % (desired_alias,)
# Check that desired_alias does not contain a period as per
# the spec.
if isinstance(desired_alias, str):
assert '.' not in desired_alias, \
"%r must not contain a dot" % (desired_alias,)
# Check that there is not a namespace already defined for
# the desired alias
current_namespace_uri = self.alias_to_namespace.get(desired_alias)
if (current_namespace_uri is not None and
current_namespace_uri != namespace_uri):
fmt = ('Cannot map %r to alias %r. '
'%r is already mapped to alias %r')
msg = fmt % (namespace_uri, desired_alias, current_namespace_uri,
desired_alias)
raise KeyError(msg)
# Check that there is not already a (different) alias for
# this namespace URI
alias = self.namespace_to_alias.get(namespace_uri)
if alias is not None and alias != desired_alias:
fmt = ('Cannot map %r to alias %r. '
'It is already mapped to alias %r')
raise KeyError(fmt % (namespace_uri, desired_alias, alias))
assert (desired_alias == NULL_NAMESPACE or
type(desired_alias) in [str, str]), repr(desired_alias)
assert namespace_uri not in self.implicit_namespaces
self.alias_to_namespace[desired_alias] = namespace_uri
self.namespace_to_alias[namespace_uri] = desired_alias
if implicit:
self.implicit_namespaces.append(namespace_uri)
return desired_alias
def add(self, namespace_uri):
"""Add this namespace URI to the mapping, without caring what
alias it ends up with"""
# See if this namespace is already mapped to an alias
alias = self.namespace_to_alias.get(namespace_uri)
if alias is not None:
return alias
# Fall back to generating a numerical alias
i = 0
while True:
alias = 'ext' + str(i)
try:
self.addAlias(namespace_uri, alias)
except KeyError:
i += 1
else:
return alias
assert False, "Not reached"
def isDefined(self, namespace_uri):
return namespace_uri in self.namespace_to_alias
def __contains__(self, namespace_uri):
return self.isDefined(namespace_uri)
def isImplicit(self, namespace_uri):
return namespace_uri in self.implicit_namespaces
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586718902.0
python3-openid-3.2.0/openid/oidutil.py 0000644 0001750 0001750 00000014711 00000000000 017604 0 ustar 00rami rami 0000000 0000000 """This module contains general utility code that is used throughout
the library.
"""
__all__ = [
'log', 'appendArgs', 'toBase64', 'fromBase64', 'autoSubmitHTML',
'toUnicode'
]
import binascii
import logging
# import urllib.parse as urlparse
from urllib.parse import urlencode
logger = logging.getLogger(__name__)
xxe_safe_elementtree_modules = [
'defusedxml.cElementTree',
'defusedxml.ElementTree',
]
elementtree_modules = [
'xml.etree.cElementTree',
'xml.etree.ElementTree',
'cElementTree',
'elementtree.ElementTree',
]
def toUnicode(value):
"""Returns the given argument as a unicode object.
@param value: A UTF-8 encoded string or a unicode (coercable) object
@type message: str or unicode
@returns: Unicode object representing the input value.
"""
if isinstance(value, bytes):
return value.decode('utf-8')
return str(value)
def autoSubmitHTML(form, title='OpenID transaction in progress'):
if isinstance(form, bytes):
form = str(form, encoding="utf-8")
if isinstance(title, bytes):
title = str(title, encoding="utf-8")
html = """
%s
%s
""" % (title, form)
return html
def importSafeElementTree(module_names=None):
"""Find a working ElementTree implementation that is not vulnerable
to XXE, using `defusedxml`.
>>> XXESafeElementTree = importSafeElementTree()
@param module_names: The names of modules to try to use as
a safe ElementTree. Defaults to C{L{xxe_safe_elementtree_modules}}
@returns: An ElementTree module that is not vulnerable to XXE.
"""
if module_names is None:
module_names = xxe_safe_elementtree_modules
try:
return importElementTree(module_names)
except ImportError:
raise ImportError('Unable to find a ElementTree module '
'that is not vulnerable to XXE. '
'Tried importing %r' % (module_names, ))
def importElementTree(module_names=None):
"""Find a working ElementTree implementation, trying the standard
places that such a thing might show up.
>>> ElementTree = importElementTree()
@param module_names: The names of modules to try to use as
ElementTree. Defaults to C{L{elementtree_modules}}
@returns: An ElementTree module
"""
if module_names is None:
module_names = elementtree_modules
for mod_name in module_names:
try:
ElementTree = __import__(mod_name, None, None, ['unused'])
except ImportError:
pass
else:
# Make sure it can actually parse XML
try:
ElementTree.XML('')
except (SystemExit, MemoryError, AssertionError):
raise
except:
logger.exception(
'Not using ElementTree library %r because it failed to '
'parse a trivial document: %s' % mod_name)
else:
return ElementTree
else:
raise ImportError('No ElementTree library found. '
'You may need to install one. '
'Tried importing %r' % (module_names, ))
def log(message, level=0):
"""Handle a log message from the OpenID library.
This is a legacy function which redirects to logger.error.
The logging module should be used instead of this
@param message: A string containing a debugging message from the
OpenID library
@type message: str
@param level: The severity of the log message. This parameter is
currently unused, but in the future, the library may indicate
more important information with a higher level value.
@type level: int or None
@returns: Nothing.
"""
logger.error("This is a legacy log message, please use the "
"logging module. Message: %s", message)
def appendArgs(url, args):
"""Append query arguments to a HTTP(s) URL. If the URL already has
query arguemtns, these arguments will be added, and the existing
arguments will be preserved. Duplicate arguments will not be
detected or collapsed (both will appear in the output).
@param url: The url to which the arguments will be appended
@type url: str
@param args: The query arguments to add to the URL. If a
dictionary is passed, the items will be sorted before
appending them to the URL. If a sequence of pairs is passed,
the order of the sequence will be preserved.
@type args: A dictionary from string to string, or a sequence of
pairs of strings.
@returns: The URL with the parameters added
@rtype: str
"""
if hasattr(args, 'items'):
args = sorted(args.items())
else:
args = list(args)
if not isinstance(url, str):
url = str(url, encoding="utf-8")
if not args:
return url
if '?' in url:
sep = '&'
else:
sep = '?'
# Map unicode to UTF-8 if present. Do not make any assumptions
# about the encodings of plain bytes (str).
i = 0
for k, v in args:
if not isinstance(k, bytes):
k = k.encode('utf-8')
if not isinstance(v, bytes):
v = v.encode('utf-8')
args[i] = (k, v)
i += 1
return '%s%s%s' % (url, sep, urlencode(args))
def toBase64(s):
"""Represent string / bytes s as base64, omitting newlines"""
if isinstance(s, str):
s = s.encode("utf-8")
return binascii.b2a_base64(s)[:-1]
def fromBase64(s):
if isinstance(s, str):
s = s.encode("utf-8")
try:
return binascii.a2b_base64(s)
except binascii.Error as why:
# Convert to a common exception type
raise ValueError(str(why))
class Symbol(object):
"""This class implements an object that compares equal to others
of the same type that have the same name. These are distict from
str or unicode objects.
"""
def __init__(self, name):
self.name = name
def __eq__(self, other):
return type(self) is type(other) and self.name == other.name
def __ne__(self, other):
return not (self == other)
def __hash__(self):
return hash((self.__class__, self.name))
def __repr__(self):
return '' % (self.name, )
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8718696
python3-openid-3.2.0/openid/server/ 0000755 0001750 0001750 00000000000 00000000000 017063 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/server/__init__.py 0000644 0001750 0001750 00000000251 00000000000 021172 0 ustar 00rami rami 0000000 0000000 """
This package contains the portions of the library used only when
implementing an OpenID server. See L{openid.server.server}.
"""
__all__ = ['server', 'trustroot']
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/openid/server/server.py 0000644 0001750 0001750 00000200111 00000000000 020736 0 ustar 00rami rami 0000000 0000000 # -*- test-case-name: openid.test.test_server -*-
"""OpenID server protocol and logic.
Overview
========
An OpenID server must perform three tasks:
1. Examine the incoming request to determine its nature and validity.
2. Make a decision about how to respond to this request.
3. Format the response according to the protocol.
The first and last of these tasks may performed by
the L{decodeRequest} and
L{encodeResponse} methods of the
L{Server} object. Who gets to do the intermediate task -- deciding
how to respond to the request -- will depend on what type of request it
is.
If it's a request to authenticate a user (a X{C{checkid_setup}} or
X{C{checkid_immediate}} request), you need to decide if you will assert
that this user may claim the identity in question. Exactly how you do
that is a matter of application policy, but it generally involves making
sure the user has an account with your system and is logged in, checking
to see if that identity is hers to claim, and verifying with the user that
she does consent to releasing that information to the party making the
request.
Examine the properties of the L{CheckIDRequest} object, optionally
check L{CheckIDRequest.returnToVerified}, and and when you've come
to a decision, form a response by calling L{CheckIDRequest.answer}.
Other types of requests relate to establishing associations between client
and server and verifying the authenticity of previous communications.
L{Server} contains all the logic and data necessary to respond to
such requests; just pass the request to L{Server.handleRequest}.
OpenID Extensions
=================
Do you want to provide other information for your users
in addition to authentication? Version 2.0 of the OpenID
protocol allows consumers to add extensions to their requests.
For example, with sites using the U{Simple Registration
Extension},
a user can agree to have their nickname and e-mail address sent to a
site when they sign up.
Since extensions do not change the way OpenID authentication works,
code to handle extension requests may be completely separate from the
L{OpenIDRequest} class here. But you'll likely want data sent back by
your extension to be signed. L{OpenIDResponse} provides methods with
which you can add data to it which can be signed with the other data in
the OpenID signature.
For example::
# when request is a checkid_* request
response = request.answer(True)
# this will a signed 'openid.sreg.timezone' parameter to the response
# as well as a namespace declaration for the openid.sreg namespace
response.fields.setArg('http://openid.net/sreg/1.0', 'timezone', 'America/Los_Angeles')
There are helper modules for a number of extensions, including
L{Attribute Exchange},
L{PAPE}, and
L{Simple Registration} in the L{openid.extensions}
package.
Stores
======
The OpenID server needs to maintain state between requests in order
to function. Its mechanism for doing this is called a store. The
store interface is defined in C{L{openid.store.interface.OpenIDStore}}.
Additionally, several concrete store implementations are provided, so that
most sites won't need to implement a custom store. For a store backed
by flat files on disk, see C{L{openid.store.filestore.FileOpenIDStore}}.
For stores based on MySQL or SQLite, see the C{L{openid.store.sqlstore}}
module.
Upgrading
=========
From 1.0 to 1.1
---------------
The keys by which a server looks up associations in its store have changed
in version 1.2 of this library. If your store has entries created from
version 1.0 code, you should empty it.
From 1.1 to 2.0
---------------
One of the additions to the OpenID protocol was a specified nonce
format for one-way nonces. As a result, the nonce table in the store
has changed. You'll need to run contrib/upgrade-store-1.1-to-2.0 to
upgrade your store, or you'll encounter errors about the wrong number
of columns in the oid_nonces table.
If you've written your own custom store or code that interacts
directly with it, you'll need to review the change notes in
L{openid.store.interface}.
@group Requests: OpenIDRequest, AssociateRequest, CheckIDRequest,
CheckAuthRequest
@group Responses: OpenIDResponse
@group HTTP Codes: HTTP_OK, HTTP_REDIRECT, HTTP_ERROR
@group Response Encodings: ENCODE_KVFORM, ENCODE_HTML_FORM, ENCODE_URL
"""
import time
import warnings
import logging
from copy import deepcopy
from openid import cryptutil
from openid import oidutil
from openid import kvform
from openid.dh import DiffieHellman
from openid.store.nonce import mkNonce
from openid.server.trustroot import TrustRoot, verifyReturnTo
from openid.association import Association, default_negotiator, getSecretSize
from openid.message import Message, InvalidOpenIDNamespace, \
OPENID_NS, OPENID2_NS, IDENTIFIER_SELECT, OPENID1_URL_LIMIT
from openid.urinorm import urinorm
logger = logging.getLogger(__name__)
HTTP_OK = 200
HTTP_REDIRECT = 302
HTTP_ERROR = 400
BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
ENCODE_KVFORM = ('kvform', )
ENCODE_URL = ('URL/redirect', )
ENCODE_HTML_FORM = ('HTML form', )
UNUSED = None
class OpenIDRequest(object):
"""I represent an incoming OpenID request.
@cvar mode: the C{X{openid.mode}} of this request.
@type mode: str
"""
mode = None
class CheckAuthRequest(OpenIDRequest):
"""A request to verify the validity of a previous response.
@cvar mode: "X{C{check_authentication}}"
@type mode: str
@ivar assoc_handle: The X{association handle} the response was signed with.
@type assoc_handle: str
@ivar signed: The message with the signature which wants checking.
@type signed: L{Message}
@ivar invalidate_handle: An X{association handle} the client is asking
about the validity of. Optional, may be C{None}.
@type invalidate_handle: str
@see: U{OpenID Specs, Mode: check_authentication
}
"""
mode = "check_authentication"
required_fields = ["identity", "return_to", "response_nonce"]
def __init__(self, assoc_handle, signed, invalidate_handle=None):
"""Construct me.
These parameters are assigned directly as class attributes, see
my L{class documentation} for their descriptions.
@type assoc_handle: str
@type signed: L{Message}
@type invalidate_handle: str
"""
self.assoc_handle = assoc_handle
self.signed = signed
self.invalidate_handle = invalidate_handle
self.namespace = OPENID2_NS
@classmethod
def fromMessage(klass, message, op_endpoint=UNUSED):
"""Construct me from an OpenID Message.
@param message: An OpenID check_authentication Message
@type message: L{openid.message.Message}
@returntype: L{CheckAuthRequest}
"""
self = klass.__new__(klass)
self.message = message
self.namespace = message.getOpenIDNamespace()
self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
self.sig = message.getArg(OPENID_NS, 'sig')
if (self.assoc_handle is None or self.sig is None):
fmt = "%s request missing required parameter from message %s"
raise ProtocolError(message, text=fmt % (self.mode, message))
self.invalidate_handle = message.getArg(OPENID_NS, 'invalidate_handle')
self.signed = message.copy()
# openid.mode is currently check_authentication because
# that's the mode of this request. But the signature
# was made on something with a different openid.mode.
# http://article.gmane.org/gmane.comp.web.openid.general/537
if self.signed.hasKey(OPENID_NS, "mode"):
self.signed.setArg(OPENID_NS, "mode", "id_res")
return self
def answer(self, signatory):
"""Respond to this request.
Given a L{Signatory}, I can check the validity of the signature and
the X{C{invalidate_handle}}.
@param signatory: The L{Signatory} to use to check the signature.
@type signatory: L{Signatory}
@returns: A response with an X{C{is_valid}} (and, if
appropriate X{C{invalidate_handle}}) field.
@returntype: L{OpenIDResponse}
"""
is_valid = signatory.verify(self.assoc_handle, self.signed)
# Now invalidate that assoc_handle so it this checkAuth message cannot
# be replayed.
signatory.invalidate(self.assoc_handle, dumb=True)
response = OpenIDResponse(self)
valid_str = (is_valid and "true") or "false"
response.fields.setArg(OPENID_NS, 'is_valid', valid_str)
if self.invalidate_handle:
assoc = signatory.getAssociation(
self.invalidate_handle, dumb=False)
if not assoc:
response.fields.setArg(OPENID_NS, 'invalidate_handle',
self.invalidate_handle)
return response
def __str__(self):
if self.invalidate_handle:
ih = " invalidate? %r" % (self.invalidate_handle, )
else:
ih = ""
s = "<%s handle: %r sig: %r: signed: %r%s>" % (
self.__class__.__name__, self.assoc_handle, self.sig, self.signed,
ih)
return s
class PlainTextServerSession(object):
"""An object that knows how to handle association requests with no
session type.
@cvar session_type: The session_type for this association
session. There is no type defined for plain-text in the OpenID
specification, so we use 'no-encryption'.
@type session_type: str
@see: U{OpenID Specs, Mode: associate
}
@see: AssociateRequest
"""
session_type = 'no-encryption'
allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
def fromMessage(cls, unused_request):
return cls()
fromMessage = classmethod(fromMessage)
def answer(self, secret):
return {'mac_key': oidutil.toBase64(secret)}
class DiffieHellmanSHA1ServerSession(object):
"""An object that knows how to handle association requests with the
Diffie-Hellman session type.
@cvar session_type: The session_type for this association
session.
@type session_type: str
@ivar dh: The Diffie-Hellman algorithm values for this request
@type dh: DiffieHellman
@ivar consumer_pubkey: The public key sent by the consumer in the
associate request
@type consumer_pubkey: long
@see: U{OpenID Specs, Mode: associate
}
@see: AssociateRequest
"""
session_type = 'DH-SHA1'
hash_func = staticmethod(cryptutil.sha1)
allowed_assoc_types = ['HMAC-SHA1']
def __init__(self, dh, consumer_pubkey):
self.dh = dh
self.consumer_pubkey = consumer_pubkey
def fromMessage(cls, message):
"""
@param message: The associate request message
@type message: openid.message.Message
@returntype: L{DiffieHellmanSHA1ServerSession}
@raises ProtocolError: When parameters required to establish the
session are missing.
"""
dh_modulus = message.getArg(OPENID_NS, 'dh_modulus')
dh_gen = message.getArg(OPENID_NS, 'dh_gen')
if (dh_modulus is None and dh_gen is not None or dh_gen is None and
dh_modulus is not None):
if dh_modulus is None:
missing = 'modulus'
else:
missing = 'generator'
raise ProtocolError(
message, 'If non-default modulus or generator is '
'supplied, both must be supplied. Missing %s' % (missing, ))
if dh_modulus or dh_gen:
dh_modulus = cryptutil.base64ToLong(dh_modulus)
dh_gen = cryptutil.base64ToLong(dh_gen)
dh = DiffieHellman(dh_modulus, dh_gen)
else:
dh = DiffieHellman.fromDefaults()
consumer_pubkey = message.getArg(OPENID_NS, 'dh_consumer_public')
if consumer_pubkey is None:
raise ProtocolError(message, "Public key for DH-SHA1 session "
"not found in message %s" % (message, ))
consumer_pubkey = cryptutil.base64ToLong(consumer_pubkey)
return cls(dh, consumer_pubkey)
fromMessage = classmethod(fromMessage)
def answer(self, secret):
mac_key = self.dh.xorSecret(self.consumer_pubkey, secret,
self.hash_func)
return {
'dh_server_public': cryptutil.longToBase64(self.dh.public),
'enc_mac_key': oidutil.toBase64(mac_key),
}
class DiffieHellmanSHA256ServerSession(DiffieHellmanSHA1ServerSession):
session_type = 'DH-SHA256'
hash_func = staticmethod(cryptutil.sha256)
allowed_assoc_types = ['HMAC-SHA256']
class AssociateRequest(OpenIDRequest):
"""A request to establish an X{association}.
@cvar mode: "X{C{check_authentication}}"
@type mode: str
@ivar assoc_type: The type of association. The protocol currently only
defines one value for this, "X{C{HMAC-SHA1}}".
@type assoc_type: str
@ivar session: An object that knows how to handle association
requests of a certain type.
@see: U{OpenID Specs, Mode: associate
}
"""
mode = "associate"
session_classes = {
'no-encryption': PlainTextServerSession,
'DH-SHA1': DiffieHellmanSHA1ServerSession,
'DH-SHA256': DiffieHellmanSHA256ServerSession,
}
def __init__(self, session, assoc_type):
"""Construct me.
The session is assigned directly as a class attribute. See my
L{class documentation} for its description.
"""
super(AssociateRequest, self).__init__()
self.session = session
self.assoc_type = assoc_type
self.namespace = OPENID2_NS
def fromMessage(klass, message, op_endpoint=UNUSED):
"""Construct me from an OpenID Message.
@param message: The OpenID associate request
@type message: openid.message.Message
@returntype: L{AssociateRequest}
"""
if message.isOpenID1():
session_type = message.getArg(OPENID_NS, 'session_type')
if session_type == 'no-encryption':
logger.warning(
'Received OpenID 1 request with a no-encryption '
'assocaition session type. Continuing anyway.')
elif not session_type:
session_type = 'no-encryption'
else:
session_type = message.getArg(OPENID2_NS, 'session_type')
if session_type is None:
raise ProtocolError(
message, text="session_type missing from request")
try:
session_class = klass.session_classes[session_type]
except KeyError:
raise ProtocolError(message,
"Unknown session type %r" % (session_type, ))
try:
session = session_class.fromMessage(message)
except ValueError as why:
raise ProtocolError(message, 'Error parsing %s session: %s' %
(session_class.session_type, why))
assoc_type = message.getArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1')
if assoc_type not in session.allowed_assoc_types:
fmt = 'Session type %s does not support association type %s'
raise ProtocolError(message, fmt % (session_type, assoc_type))
self = klass(session, assoc_type)
self.message = message
self.namespace = message.getOpenIDNamespace()
return self
fromMessage = classmethod(fromMessage)
def answer(self, assoc):
"""Respond to this request with an X{association}.
@param assoc: The association to send back.
@type assoc: L{openid.association.Association}
@returns: A response with the association information, encrypted
to the consumer's X{public key} if appropriate.
@returntype: L{OpenIDResponse}
"""
response = OpenIDResponse(self)
response.fields.updateArgs(OPENID_NS, {
'expires_in': str(assoc.expiresIn),
'assoc_type': self.assoc_type,
'assoc_handle': assoc.handle,
})
response.fields.updateArgs(OPENID_NS,
self.session.answer(assoc.secret))
if not (self.session.session_type == 'no-encryption' and
self.message.isOpenID1()):
# The session type "no-encryption" did not have a name
# in OpenID v1, it was just omitted.
response.fields.setArg(OPENID_NS, 'session_type',
self.session.session_type)
return response
def answerUnsupported(self,
message,
preferred_association_type=None,
preferred_session_type=None):
"""Respond to this request indicating that the association
type or association session type is not supported."""
if self.message.isOpenID1():
raise ProtocolError(self.message)
response = OpenIDResponse(self)
response.fields.setArg(OPENID_NS, 'error_code', 'unsupported-type')
response.fields.setArg(OPENID_NS, 'error', message)
if preferred_association_type:
response.fields.setArg(OPENID_NS, 'assoc_type',
preferred_association_type)
if preferred_session_type:
response.fields.setArg(OPENID_NS, 'session_type',
preferred_session_type)
return response
class CheckIDRequest(OpenIDRequest):
"""A request to confirm the identity of a user.
This class handles requests for openid modes X{C{checkid_immediate}}
and X{C{checkid_setup}}.
@cvar mode: "X{C{checkid_immediate}}" or "X{C{checkid_setup}}"
@type mode: str
@ivar immediate: Is this an immediate-mode request?
@type immediate: bool
@ivar identity: The OP-local identifier being checked.
@type identity: str
@ivar claimed_id: The claimed identifier. Not present in OpenID 1.x
messages.
@type claimed_id: str
@ivar trust_root: "Are you Frank?" asks the checkid request. "Who wants
to know?" C{trust_root}, that's who. This URL identifies the party
making the request, and the user will use that to make her decision
about what answer she trusts them to have. Referred to as "realm" in
OpenID 2.0.
@type trust_root: str
@ivar return_to: The URL to send the user agent back to to reply to this
request.
@type return_to: str
@ivar assoc_handle: Provided in smart mode requests, a handle for a
previously established association. C{None} for dumb mode requests.
@type assoc_handle: str
"""
def __init__(self,
identity,
return_to,
trust_root=None,
immediate=False,
assoc_handle=None,
op_endpoint=None,
claimed_id=None):
"""Construct me.
These parameters are assigned directly as class attributes, see
my L{class documentation} for their descriptions.
@raises MalformedReturnURL: When the C{return_to} URL is not a URL.
"""
self.assoc_handle = assoc_handle
self.identity = identity
self.claimed_id = claimed_id or identity
self.return_to = return_to
self.trust_root = trust_root or return_to
self.op_endpoint = op_endpoint
assert self.op_endpoint is not None
if immediate:
self.immediate = True
self.mode = "checkid_immediate"
else:
self.immediate = False
self.mode = "checkid_setup"
if self.return_to is not None and \
not TrustRoot.parse(self.return_to):
raise MalformedReturnURL(None, self.return_to)
if not self.trustRootValid():
raise UntrustedReturnURL(None, self.return_to, self.trust_root)
self.message = None
def _getNamespace(self):
warnings.warn(
'The "namespace" attribute of CheckIDRequest objects '
'is deprecated. Use "message.getOpenIDNamespace()" '
'instead',
DeprecationWarning,
stacklevel=2)
return self.message.getOpenIDNamespace()
namespace = property(_getNamespace)
def fromMessage(klass, message, op_endpoint):
"""Construct me from an OpenID message.
@raises ProtocolError: When not all required parameters are present
in the message.
@raises MalformedReturnURL: When the C{return_to} URL is not a URL.
@raises UntrustedReturnURL: When the C{return_to} URL is outside
the C{trust_root}.
@param message: An OpenID checkid_* request Message
@type message: openid.message.Message
@param op_endpoint: The endpoint URL of the server that this
message was sent to.
@type op_endpoint: str
@returntype: L{CheckIDRequest}
"""
self = klass.__new__(klass)
self.message = message
self.op_endpoint = op_endpoint
mode = message.getArg(OPENID_NS, 'mode')
if mode == "checkid_immediate":
self.immediate = True
self.mode = "checkid_immediate"
else:
self.immediate = False
self.mode = "checkid_setup"
self.return_to = message.getArg(OPENID_NS, 'return_to')
if message.isOpenID1() and not self.return_to:
fmt = "Missing required field 'return_to' from %r"
raise ProtocolError(message, text=fmt % (message, ))
self.identity = message.getArg(OPENID_NS, 'identity')
self.claimed_id = message.getArg(OPENID_NS, 'claimed_id')
if message.isOpenID1():
if self.identity is None:
s = "OpenID 1 message did not contain openid.identity"
raise ProtocolError(message, text=s)
else:
if self.identity and not self.claimed_id:
s = ("OpenID 2.0 message contained openid.identity but not "
"claimed_id")
raise ProtocolError(message, text=s)
elif self.claimed_id and not self.identity:
s = ("OpenID 2.0 message contained openid.claimed_id but not "
"identity")
raise ProtocolError(message, text=s)
# There's a case for making self.trust_root be a TrustRoot
# here. But if TrustRoot isn't currently part of the "public" API,
# I'm not sure it's worth doing.
if message.isOpenID1():
trust_root_param = 'trust_root'
else:
trust_root_param = 'realm'
# Using 'or' here is slightly different than sending a default
# argument to getArg, as it will treat no value and an empty
# string as equivalent.
self.trust_root = (message.getArg(OPENID_NS, trust_root_param) or
self.return_to)
if not message.isOpenID1():
if self.return_to is self.trust_root is None:
raise ProtocolError(
message,
"openid.realm required when " + "openid.return_to absent")
self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle')
# Using TrustRoot.parse here is a bit misleading, as we're not
# parsing return_to as a trust root at all. However, valid URLs
# are valid trust roots, so we can use this to get an idea if it
# is a valid URL. Not all trust roots are valid return_to URLs,
# however (particularly ones with wildcards), so this is still a
# little sketchy.
if self.return_to is not None and \
not TrustRoot.parse(self.return_to):
raise MalformedReturnURL(message, self.return_to)
# I first thought that checking to see if the return_to is within
# the trust_root is premature here, a logic-not-decoding thing. But
# it was argued that this is really part of data validation. A
# request with an invalid trust_root/return_to is broken regardless of
# application, right?
if not self.trustRootValid():
raise UntrustedReturnURL(message, self.return_to, self.trust_root)
return self
fromMessage = classmethod(fromMessage)
def idSelect(self):
"""Is the identifier to be selected by the IDP?
@returntype: bool
"""
# So IDPs don't have to import the constant
return self.identity == IDENTIFIER_SELECT
def trustRootValid(self):
"""Is my return_to under my trust_root?
@returntype: bool
"""
if not self.trust_root:
return True
tr = TrustRoot.parse(self.trust_root)
if tr is None:
raise MalformedTrustRoot(self.message, self.trust_root)
if self.return_to is not None:
return tr.validateURL(self.return_to)
else:
return True
def returnToVerified(self):
"""Does the relying party publish the return_to URL for this
response under the realm? It is up to the provider to set a
policy for what kinds of realms should be allowed. This
return_to URL verification reduces vulnerability to data-theft
attacks based on open proxies, cross-site-scripting, or open
redirectors.
This check should only be performed after making sure that the
return_to URL matches the realm.
@see: L{trustRootValid}
@raises openid.yadis.discover.DiscoveryFailure: if the realm
URL does not support Yadis discovery (and so does not
support the verification process).
@raises openid.fetchers.HTTPFetchingError: if the realm URL
is not reachable. When this is the case, the RP may be hosted
on the user's intranet.
@returntype: bool
@returns: True if the realm publishes a document with the
return_to URL listed
@since: 2.1.0
"""
return verifyReturnTo(self.trust_root, self.return_to)
def answer(self, allow, server_url=None, identity=None, claimed_id=None):
"""Respond to this request.
@param allow: Allow this user to claim this identity, and allow the
consumer to have this information?
@type allow: bool
@param server_url: DEPRECATED. Passing C{op_endpoint} to the
L{Server} constructor makes this optional.
When an OpenID 1.x immediate mode request does not succeed,
it gets back a URL where the request may be carried out
in a not-so-immediate fashion. Pass my URL in here (the
fully qualified address of this server's endpoint, i.e.
C{http://example.com/server}), and I will use it as a base for the
URL for a new request.
Optional for requests where C{CheckIDRequest.immediate} is C{False}
or C{allow} is C{True}.
@type server_url: str
@param identity: The OP-local identifier to answer with. Only for use
when the relying party requested identifier selection.
@type identity: str or None
@param claimed_id: The claimed identifier to answer with, for use
with identifier selection in the case where the claimed identifier
and the OP-local identifier differ, i.e. when the claimed_id uses
delegation.
If C{identity} is provided but this is not, C{claimed_id} will
default to the value of C{identity}. When answering requests
that did not ask for identifier selection, the response
C{claimed_id} will default to that of the request.
This parameter is new in OpenID 2.0.
@type claimed_id: str or None
@returntype: L{OpenIDResponse}
@change: Version 2.0 deprecates C{server_url} and adds C{claimed_id}.
@raises NoReturnError: when I do not have a return_to.
"""
assert self.message is not None
if not self.return_to:
raise NoReturnToError
if not server_url:
if not self.message.isOpenID1() and not self.op_endpoint:
# In other words, that warning I raised in Server.__init__?
# You should pay attention to it now.
raise RuntimeError("%s should be constructed with op_endpoint "
"to respond to OpenID 2.0 messages." %
(self, ))
server_url = self.op_endpoint
if allow:
mode = 'id_res'
elif self.message.isOpenID1():
if self.immediate:
mode = 'id_res'
else:
mode = 'cancel'
else:
if self.immediate:
mode = 'setup_needed'
else:
mode = 'cancel'
response = OpenIDResponse(self)
if claimed_id and self.message.isOpenID1():
namespace = self.message.getOpenIDNamespace()
raise VersionError("claimed_id is new in OpenID 2.0 and not "
"available for %s" % (namespace, ))
if allow:
if self.identity == IDENTIFIER_SELECT:
if not identity:
raise ValueError(
"This request uses IdP-driven identifier selection."
"You must supply an identifier in the response.")
response_identity = identity
response_claimed_id = claimed_id or identity
elif self.identity:
if identity and (self.identity != identity):
normalized_request_identity = urinorm(self.identity)
normalized_answer_identity = urinorm(identity)
if (normalized_request_identity !=
normalized_answer_identity):
raise ValueError(
"Request was for identity %r, cannot reply "
"with identity %r" % (self.identity, identity))
# The "identity" value in the response shall always be
# the same as that in the request, otherwise the RP is
# likely to not validate the response.
response_identity = self.identity
response_claimed_id = self.claimed_id
else:
if identity:
raise ValueError(
"This request specified no identity and you "
"supplied %r" % (identity, ))
response_identity = None
if self.message.isOpenID1() and response_identity is None:
raise ValueError(
"Request was an OpenID 1 request, so response must "
"include an identifier.")
response.fields.updateArgs(OPENID_NS, {
'mode': mode,
'return_to': self.return_to,
'response_nonce': mkNonce(),
})
if server_url:
response.fields.setArg(OPENID_NS, 'op_endpoint', server_url)
if response_identity is not None:
response.fields.setArg(OPENID_NS, 'identity',
response_identity)
if self.message.isOpenID2():
response.fields.setArg(OPENID_NS, 'claimed_id',
response_claimed_id)
else:
response.fields.setArg(OPENID_NS, 'mode', mode)
if self.immediate:
if self.message.isOpenID1() and not server_url:
raise ValueError("setup_url is required for allow=False "
"in OpenID 1.x immediate mode.")
# Make a new request just like me, but with immediate=False.
setup_request = self.__class__(
self.identity,
self.return_to,
self.trust_root,
immediate=False,
assoc_handle=self.assoc_handle,
op_endpoint=self.op_endpoint,
claimed_id=self.claimed_id)
# XXX: This API is weird.
setup_request.message = self.message
setup_url = setup_request.encodeToURL(server_url)
response.fields.setArg(OPENID_NS, 'user_setup_url', setup_url)
return response
def encodeToURL(self, server_url):
"""Encode this request as a URL to GET.
@param server_url: URL of the OpenID server to make this request of.
@type server_url: str
@returntype: str
@raises NoReturnError: when I do not have a return_to.
"""
if not self.return_to:
raise NoReturnToError
# Imported from the alternate reality where these classes are used
# in both the client and server code, so Requests are Encodable too.
# That's right, code imported from alternate realities all for the
# love of you, id_res/user_setup_url.
q = {
'mode': self.mode,
'identity': self.identity,
'claimed_id': self.claimed_id,
'return_to': self.return_to
}
if self.trust_root:
if self.message.isOpenID1():
q['trust_root'] = self.trust_root
else:
q['realm'] = self.trust_root
if self.assoc_handle:
q['assoc_handle'] = self.assoc_handle
response = Message(self.message.getOpenIDNamespace())
response.updateArgs(OPENID_NS, q)
return response.toURL(server_url)
def getCancelURL(self):
"""Get the URL to cancel this request.
Useful for creating a "Cancel" button on a web form so that operation
can be carried out directly without another trip through the server.
(Except you probably want to make another trip through the server so
that it knows that the user did make a decision. Or you could simulate
this method by doing C{.answer(False).encodeToURL()})
@returntype: str
@returns: The return_to URL with openid.mode = cancel.
@raises NoReturnError: when I do not have a return_to.
"""
if not self.return_to:
raise NoReturnToError
if self.immediate:
raise ValueError("Cancel is not an appropriate response to "
"immediate mode requests.")
response = Message(self.message.getOpenIDNamespace())
response.setArg(OPENID_NS, 'mode', 'cancel')
return response.toURL(self.return_to)
def __repr__(self):
return '<%s id:%r im:%s tr:%r ah:%r>' % (
self.__class__.__name__, self.identity, self.immediate,
self.trust_root, self.assoc_handle)
class OpenIDResponse(object):
"""I am a response to an OpenID request.
@ivar request: The request I respond to.
@type request: L{OpenIDRequest}
@ivar fields: My parameters as a dictionary with each key mapping to
one value. Keys are parameter names with no leading "C{openid.}".
e.g. "C{identity}" and "C{mac_key}", never "C{openid.identity}".
@type fields: L{openid.message.Message}
@ivar signed: The names of the fields which should be signed.
@type signed: list of str
"""
# Implementer's note: In a more symmetric client/server
# implementation, there would be more types of OpenIDResponse
# object and they would have validated attributes according to the
# type of response. But as it is, Response objects in a server are
# basically write-only, their only job is to go out over the wire,
# so this is just a loose wrapper around OpenIDResponse.fields.
def __init__(self, request):
"""Make a response to an L{OpenIDRequest}.
@type request: L{OpenIDRequest}
"""
self.request = request
self.fields = Message(request.namespace)
def __str__(self):
return "%s for %s: %s" % (self.__class__.__name__,
self.request.__class__.__name__, self.fields)
def toFormMarkup(self, form_tag_attrs=None):
"""Returns the form markup for this response.
@param form_tag_attrs: Dictionary of attributes to be added to
the form tag. 'accept-charset' and 'enctype' have defaults
that can be overridden. If a value is supplied for
'action' or 'method', it will be replaced.
@returntype: str
@since: 2.1.0
"""
return self.fields.toFormMarkup(
self.request.return_to, form_tag_attrs=form_tag_attrs)
def toHTML(self, form_tag_attrs=None):
"""Returns an HTML document that auto-submits the form markup
for this response.
@returntype: str
@see: toFormMarkup
@since: 2.1.?
"""
return oidutil.autoSubmitHTML(self.toFormMarkup(form_tag_attrs))
def renderAsForm(self):
"""Returns True if this response's encoding is
ENCODE_HTML_FORM. Convenience method for server authors.
@returntype: bool
@since: 2.1.0
"""
return self.whichEncoding() == ENCODE_HTML_FORM
def needsSigning(self):
"""Does this response require signing?
@returntype: bool
"""
return self.fields.getArg(OPENID_NS, 'mode') == 'id_res'
# implements IEncodable
def whichEncoding(self):
"""How should I be encoded?
@returns: one of ENCODE_URL, ENCODE_HTML_FORM, or ENCODE_KVFORM.
@change: 2.1.0 added the ENCODE_HTML_FORM response.
"""
if self.request.mode in BROWSER_REQUEST_MODES:
if self.fields.getOpenIDNamespace() == OPENID2_NS and \
len(self.encodeToURL()) > OPENID1_URL_LIMIT:
return ENCODE_HTML_FORM
else:
return ENCODE_URL
else:
return ENCODE_KVFORM
def encodeToURL(self):
"""Encode a response as a URL for the user agent to GET.
You will generally use this URL with a HTTP redirect.
@returns: A URL to direct the user agent back to.
@returntype: str
"""
return self.fields.toURL(self.request.return_to)
def addExtension(self, extension_response):
"""
Add an extension response to this response message.
@param extension_response: An object that implements the
extension interface for adding arguments to an OpenID
message.
@type extension_response: L{openid.extension}
@returntype: None
"""
extension_response.toMessage(self.fields)
def encodeToKVForm(self):
"""Encode a response in key-value colon/newline format.
This is a machine-readable format used to respond to messages which
came directly from the consumer and not through the user agent.
@see: OpenID Specs,
U{Key-Value Colon/Newline format}
@returntype: str
"""
return self.fields.toKVForm()
class WebResponse(object):
"""I am a response to an OpenID request in terms a web server understands.
I generally come from an L{Encoder}, either directly or from
L{Server.encodeResponse}.
@ivar code: The HTTP code of this response.
@type code: int
@ivar headers: Headers to include in this response.
@type headers: dict
@ivar body: The body of this response.
@type body: str
"""
def __init__(self, code=HTTP_OK, headers=None, body=b""):
"""Construct me.
These parameters are assigned directly as class attributes, see
my L{class documentation} for their descriptions.
"""
self.code = code
if headers is not None:
self.headers = headers
else:
self.headers = {}
if isinstance(body, bytes):
body = str(body, encoding="utf-8")
self.body = body
class Signatory(object):
"""I sign things.
I also check signatures.
All my state is encapsulated in an
L{OpenIDStore}, which means
I'm not generally pickleable but I am easy to reconstruct.
@cvar SECRET_LIFETIME: The number of seconds a secret remains valid.
@type SECRET_LIFETIME: int
"""
SECRET_LIFETIME = 14 * 24 * 60 * 60 # 14 days, in seconds
# keys have a bogus server URL in them because the filestore
# really does expect that key to be a URL. This seems a little
# silly for the server store, since I expect there to be only one
# server URL.
_normal_key = 'http://localhost/|normal'
_dumb_key = 'http://localhost/|dumb'
def __init__(self, store):
"""Create a new Signatory.
@param store: The back-end where my associations are stored.
@type store: L{openid.store.interface.OpenIDStore}
"""
assert store is not None
self.store = store
def verify(self, assoc_handle, message):
"""Verify that the signature for some data is valid.
@param assoc_handle: The handle of the association used to sign the
data.
@type assoc_handle: str
@param message: The signed message to verify
@type message: openid.message.Message
@returns: C{True} if the signature is valid, C{False} if not.
@returntype: bool
"""
assoc = self.getAssociation(assoc_handle, dumb=True)
if not assoc:
logger.error("failed to get assoc with handle %r to verify "
"message %r" % (assoc_handle, message))
return False
try:
valid = assoc.checkMessageSignature(message)
except ValueError as ex:
logger.exception("Error in verifying %s with %s: %s" %
(message, assoc, ex))
return False
return valid
def sign(self, response):
"""Sign a response.
I take a L{OpenIDResponse}, create a signature for everything
in its L{signed} list, and return a new
copy of the response object with that signature included.
@param response: A response to sign.
@type response: L{OpenIDResponse}
@returns: A signed copy of the response.
@returntype: L{OpenIDResponse}
"""
signed_response = deepcopy(response)
assoc_handle = response.request.assoc_handle
if assoc_handle:
# normal mode
# disabling expiration check because even if the association
# is expired, we still need to know some properties of the
# association so that we may preserve those properties when
# creating the fallback association.
assoc = self.getAssociation(
assoc_handle, dumb=False, checkExpiration=False)
if not assoc or assoc.expiresIn <= 0:
# fall back to dumb mode
signed_response.fields.setArg(OPENID_NS, 'invalidate_handle',
assoc_handle)
assoc_type = assoc and assoc.assoc_type or 'HMAC-SHA1'
if assoc and assoc.expiresIn <= 0:
# now do the clean-up that the disabled checkExpiration
# code didn't get to do.
self.invalidate(assoc_handle, dumb=False)
assoc = self.createAssociation(
dumb=True, assoc_type=assoc_type)
else:
# dumb mode.
assoc = self.createAssociation(dumb=True)
try:
signed_response.fields = assoc.signMessage(signed_response.fields)
except kvform.KVFormError as err:
raise EncodingError(response, explanation=str(err))
return signed_response
def createAssociation(self, dumb=True, assoc_type='HMAC-SHA1'):
"""Make a new association.
@param dumb: Is this association for a dumb-mode transaction?
@type dumb: bool
@param assoc_type: The type of association to create. Currently
there is only one type defined, C{HMAC-SHA1}.
@type assoc_type: str
@returns: the new association.
@returntype: L{openid.association.Association}
"""
secret = cryptutil.getBytes(getSecretSize(assoc_type))
uniq = oidutil.toBase64(cryptutil.getBytes(4))
handle = '{%s}{%x}{%s}' % (assoc_type, int(time.time()), uniq)
assoc = Association.fromExpiresIn(self.SECRET_LIFETIME, handle, secret,
assoc_type)
if dumb:
key = self._dumb_key
else:
key = self._normal_key
self.store.storeAssociation(key, assoc)
return assoc
def getAssociation(self, assoc_handle, dumb, checkExpiration=True):
"""Get the association with the specified handle.
@type assoc_handle: str
@param dumb: Is this association used with dumb mode?
@type dumb: bool
@returns: the association, or None if no valid association with that
handle was found.
@returntype: L{openid.association.Association}
"""
# Hmm. We've created an interface that deals almost entirely with
# assoc_handles. The only place outside the Signatory that uses this
# (and thus the only place that ever sees Association objects) is
# when creating a response to an association request, as it must have
# the association's secret.
if assoc_handle is None:
raise ValueError("assoc_handle must not be None")
if dumb:
key = self._dumb_key
else:
key = self._normal_key
assoc = self.store.getAssociation(key, assoc_handle)
if assoc is not None and assoc.expiresIn <= 0:
logger.info("requested %sdumb key %r is expired (by %s seconds)" %
((not dumb) and 'not-' or '', assoc_handle,
assoc.expiresIn))
if checkExpiration:
self.store.removeAssociation(key, assoc_handle)
assoc = None
return assoc
def invalidate(self, assoc_handle, dumb):
"""Invalidates the association with the given handle.
@type assoc_handle: str
@param dumb: Is this association used with dumb mode?
@type dumb: bool
"""
if dumb:
key = self._dumb_key
else:
key = self._normal_key
self.store.removeAssociation(key, assoc_handle)
class Encoder(object):
"""I encode responses in to L{WebResponses}.
If you don't like L{WebResponses}, you can do
your own handling of L{OpenIDResponses} with
L{OpenIDResponse.whichEncoding}, L{OpenIDResponse.encodeToURL}, and
L{OpenIDResponse.encodeToKVForm}.
"""
responseFactory = WebResponse
def encode(self, response):
"""Encode a response to a L{WebResponse}.
@raises EncodingError: When I can't figure out how to encode this
message.
"""
encode_as = response.whichEncoding()
if encode_as == ENCODE_KVFORM:
wr = self.responseFactory(body=response.encodeToKVForm())
if isinstance(response, Exception):
wr.code = HTTP_ERROR
elif encode_as == ENCODE_URL:
location = response.encodeToURL()
wr = self.responseFactory(
code=HTTP_REDIRECT, headers={'location': location})
elif encode_as == ENCODE_HTML_FORM:
wr = self.responseFactory(code=HTTP_OK, body=response.toHTML())
else:
# Can't encode this to a protocol message. You should probably
# render it to HTML and show it to the user.
raise EncodingError(response)
return wr
class SigningEncoder(Encoder):
"""I encode responses in to L{WebResponses}, signing them when required.
"""
def __init__(self, signatory):
"""Create a L{SigningEncoder}.
@param signatory: The L{Signatory} I will make signatures with.
@type signatory: L{Signatory}
"""
self.signatory = signatory
def encode(self, response):
"""Encode a response to a L{WebResponse}, signing it first if appropriate.
@raises EncodingError: When I can't figure out how to encode this
message.
@raises AlreadySigned: When this response is already signed.
@returntype: L{WebResponse}
"""
# the isinstance is a bit of a kludge... it means there isn't really
# an adapter to make the interfaces quite match.
if (not isinstance(response, Exception)) and response.needsSigning():
if not self.signatory:
raise ValueError("Must have a store to sign this request: %s" %
(response, ), response)
if response.fields.hasKey(OPENID_NS, 'sig'):
raise AlreadySigned(response)
response = self.signatory.sign(response)
return super(SigningEncoder, self).encode(response)
class Decoder(object):
"""I decode an incoming web request in to a L{OpenIDRequest}.
"""
_handlers = {
'checkid_setup': CheckIDRequest.fromMessage,
'checkid_immediate': CheckIDRequest.fromMessage,
'check_authentication': CheckAuthRequest.fromMessage,
'associate': AssociateRequest.fromMessage,
}
def __init__(self, server):
"""Construct a Decoder.
@param server: The server which I am decoding requests for.
(Necessary because some replies reference their server.)
@type server: L{Server}
"""
self.server = server
def decode(self, query):
"""I transform query parameters into an L{OpenIDRequest}.
If the query does not seem to be an OpenID request at all, I return
C{None}.
@param query: The query parameters as a dictionary with each
key mapping to one value.
@type query: dict
@raises ProtocolError: When the query does not seem to be a valid
OpenID request.
@returntype: L{OpenIDRequest}
"""
if not query:
return None
try:
message = Message.fromPostArgs(query)
except InvalidOpenIDNamespace as err:
# It's useful to have a Message attached to a ProtocolError, so we
# override the bad ns value to build a Message out of it. Kinda
# kludgy, since it's made of lies, but the parts that aren't lies
# are more useful than a 'None'.
query = query.copy()
query['openid.ns'] = OPENID2_NS
message = Message.fromPostArgs(query)
raise ProtocolError(message, str(err))
mode = message.getArg(OPENID_NS, 'mode')
if not mode:
fmt = "No mode value in message %s"
raise ProtocolError(message, text=fmt % (message, ))
handler = self._handlers.get(mode, self.defaultDecoder)
return handler(message, self.server.op_endpoint)
def defaultDecoder(self, message, server):
"""Called to decode queries when no handler for that mode is found.
@raises ProtocolError: This implementation always raises
L{ProtocolError}.
"""
mode = message.getArg(OPENID_NS, 'mode')
fmt = "Unrecognized OpenID mode %r"
raise ProtocolError(message, text=fmt % (mode, ))
class Server(object):
"""I handle requests for an OpenID server.
Some types of requests (those which are not C{checkid} requests) may be
handed to my L{handleRequest} method, and I will take care of it and
return a response.
For your convenience, I also provide an interface to L{Decoder.decode}
and L{SigningEncoder.encode} through my methods L{decodeRequest} and
L{encodeResponse}.
All my state is encapsulated in an
L{OpenIDStore}, which means
I'm not generally pickleable but I am easy to reconstruct.
Example::
oserver = Server(FileOpenIDStore(data_path), "http://example.com/op")
request = oserver.decodeRequest(query)
if request.mode in ['checkid_immediate', 'checkid_setup']:
if self.isAuthorized(request.identity, request.trust_root):
response = request.answer(True)
elif request.immediate:
response = request.answer(False)
else:
self.showDecidePage(request)
return
else:
response = oserver.handleRequest(request)
webresponse = oserver.encode(response)
@ivar signatory: I'm using this for associate requests and to sign things.
@type signatory: L{Signatory}
@ivar decoder: I'm using this to decode things.
@type decoder: L{Decoder}
@ivar encoder: I'm using this to encode things.
@type encoder: L{Encoder}
@ivar op_endpoint: My URL.
@type op_endpoint: str
@ivar negotiator: I use this to determine which kinds of
associations I can make and how.
@type negotiator: L{openid.association.SessionNegotiator}
"""
def __init__(self,
store,
op_endpoint=None,
signatoryClass=Signatory,
encoderClass=SigningEncoder,
decoderClass=Decoder):
"""A new L{Server}.
@param store: The back-end where my associations are stored.
@type store: L{openid.store.interface.OpenIDStore}
@param op_endpoint: My URL, the fully qualified address of this
server's endpoint, i.e. C{http://example.com/server}
@type op_endpoint: str
@change: C{op_endpoint} is new in library version 2.0. It
currently defaults to C{None} for compatibility with
earlier versions of the library, but you must provide it
if you want to respond to any version 2 OpenID requests.
"""
self.store = store
self.signatory = signatoryClass(self.store)
self.encoder = encoderClass(self.signatory)
self.decoder = decoderClass(self)
self.negotiator = default_negotiator.copy()
if not op_endpoint:
warnings.warn(
"%s.%s constructor requires op_endpoint parameter "
"for OpenID 2.0 servers" %
(self.__class__.__module__, self.__class__.__name__),
stacklevel=2)
self.op_endpoint = op_endpoint
def handleRequest(self, request):
"""Handle a request.
Give me a request, I will give you a response. Unless it's a type
of request I cannot handle myself, in which case I will raise
C{NotImplementedError}. In that case, you can handle it yourself,
or add a method to me for handling that request type.
@raises NotImplementedError: When I do not have a handler defined
for that type of request.
@returntype: L{OpenIDResponse}
"""
handler = getattr(self, 'openid_' + request.mode, None)
if handler is not None:
return handler(request)
else:
raise NotImplementedError(
"%s has no handler for a request of mode %r." %
(self, request.mode))
def openid_check_authentication(self, request):
"""Handle and respond to C{check_authentication} requests.
@returntype: L{OpenIDResponse}
"""
return request.answer(self.signatory)
def openid_associate(self, request):
"""Handle and respond to C{associate} requests.
@returntype: L{OpenIDResponse}
"""
# XXX: TESTME
assoc_type = request.assoc_type
session_type = request.session.session_type
if self.negotiator.isAllowed(assoc_type, session_type):
assoc = self.signatory.createAssociation(
dumb=False, assoc_type=assoc_type)
return request.answer(assoc)
else:
message = ('Association type %r is not supported with '
'session type %r' % (assoc_type, session_type))
(preferred_assoc_type, preferred_session_type) = \
self.negotiator.getAllowedType()
return request.answerUnsupported(message, preferred_assoc_type,
preferred_session_type)
def decodeRequest(self, query):
"""Transform query parameters into an L{OpenIDRequest}.
If the query does not seem to be an OpenID request at all, I return
C{None}.
@param query: The query parameters as a dictionary with each
key mapping to one value.
@type query: dict
@raises ProtocolError: When the query does not seem to be a valid
OpenID request.
@returntype: L{OpenIDRequest}
@see: L{Decoder.decode}
"""
return self.decoder.decode(query)
def encodeResponse(self, response):
"""Encode a response to a L{WebResponse}, signing it first if appropriate.
@raises EncodingError: When I can't figure out how to encode this
message.
@raises AlreadySigned: When this response is already signed.
@returntype: L{WebResponse}
@see: L{SigningEncoder.encode}
"""
return self.encoder.encode(response)
class ProtocolError(Exception):
"""A message did not conform to the OpenID protocol.
@ivar message: The query that is failing to be a valid OpenID request.
@type message: openid.message.Message
"""
def __init__(self, message, text=None, reference=None, contact=None):
"""When an error occurs.
@param message: The message that is failing to be a valid
OpenID request.
@type message: openid.message.Message
@param text: A message about the encountered error. Set as C{args[0]}.
@type text: str
"""
self.openid_message = message
self.reference = reference
self.contact = contact
assert type(message) not in [str, str]
Exception.__init__(self, text)
def getReturnTo(self):
"""Get the return_to argument from the request, if any.
@returntype: str
"""
if self.openid_message is None:
return None
else:
return self.openid_message.getArg(OPENID_NS, 'return_to')
def hasReturnTo(self):
"""Did this request have a return_to parameter?
@returntype: bool
"""
return self.getReturnTo() is not None
def toMessage(self):
"""Generate a Message object for sending to the relying party,
after encoding.
"""
namespace = self.openid_message.getOpenIDNamespace()
reply = Message(namespace)
reply.setArg(OPENID_NS, 'mode', 'error')
reply.setArg(OPENID_NS, 'error', str(self))
if self.contact is not None:
reply.setArg(OPENID_NS, 'contact', str(self.contact))
if self.reference is not None:
reply.setArg(OPENID_NS, 'reference', str(self.reference))
return reply
# implements IEncodable
def encodeToURL(self):
return self.toMessage().toURL(self.getReturnTo())
def encodeToKVForm(self):
return self.toMessage().toKVForm()
def toFormMarkup(self):
"""Encode to HTML form markup for POST.
@since: 2.1.0
"""
return self.toMessage().toFormMarkup(self.getReturnTo())
def toHTML(self):
"""Encode to a full HTML page, wrapping the form markup in a page
that will autosubmit the form.
@since: 2.1.?
"""
return oidutil.autoSubmitHTML(self.toFormMarkup())
def whichEncoding(self):
"""How should I be encoded?
@returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None,
I cannot be encoded as a protocol message and should be
displayed to the user.
"""
if self.hasReturnTo():
if self.openid_message.getOpenIDNamespace() == OPENID2_NS and \
len(self.encodeToURL()) > OPENID1_URL_LIMIT:
return ENCODE_HTML_FORM
else:
return ENCODE_URL
if self.openid_message is None:
return None
mode = self.openid_message.getArg(OPENID_NS, 'mode')
if mode:
if mode not in BROWSER_REQUEST_MODES:
return ENCODE_KVFORM
# According to the OpenID spec as of this writing, we are probably
# supposed to switch on request type here (GET versus POST) to figure
# out if we're supposed to print machine-readable or human-readable
# content at this point. GET/POST seems like a pretty lousy way of
# making the distinction though, as it's just as possible that the
# user agent could have mistakenly been directed to post to the
# server URL.
# Basically, if your request was so broken that you didn't manage to
# include an openid.mode, I'm not going to worry too much about
# returning you something you can't parse.
return None
class VersionError(Exception):
"""Raised when an operation was attempted that is not compatible with
the protocol version being used."""
class NoReturnToError(Exception):
"""Raised when a response to a request cannot be generated because
the request contains no return_to URL.
"""
pass
class EncodingError(Exception):
"""Could not encode this as a protocol message.
You should probably render it and show it to the user.
@ivar response: The response that failed to encode.
@type response: L{OpenIDResponse}
"""
def __init__(self, response, explanation=None):
Exception.__init__(self, response)
self.response = response
self.explanation = explanation
def __str__(self):
if self.explanation:
s = '%s: %s' % (self.__class__.__name__, self.explanation)
else:
s = '%s for Response %s' % (self.__class__.__name__, self.response)
return s
class AlreadySigned(EncodingError):
"""This response is already signed."""
class UntrustedReturnURL(ProtocolError):
"""A return_to is outside the trust_root."""
def __init__(self, message, return_to, trust_root):
ProtocolError.__init__(self, message)
self.return_to = return_to
self.trust_root = trust_root
def __str__(self):
return "return_to %r not under trust_root %r" % (self.return_to,
self.trust_root)
class MalformedReturnURL(ProtocolError):
"""The return_to URL doesn't look like a valid URL."""
def __init__(self, openid_message, return_to):
self.return_to = return_to
ProtocolError.__init__(self, openid_message)
class MalformedTrustRoot(ProtocolError):
"""The trust root is not well-formed.
@see: OpenID Specs, U{openid.trust_root}
"""
pass
#class IEncodable: # Interface
# def encodeToURL(return_to):
# """Encode a response as a URL for redirection.
#
# @returns: A URL to direct the user agent back to.
# @returntype: str
# """
# pass
#
# def encodeToKvform():
# """Encode a response in key-value colon/newline format.
#
# This is a machine-readable format used to respond to messages which
# came directly from the consumer and not through the user agent.
#
# @see: OpenID Specs,
# U{Key-Value Colon/Newline format}
#
# @returntype: str
# """
# pass
#
# def whichEncoding():
# """How should I be encoded?
#
# @returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None,
# I cannot be encoded as a protocol message and should be
# displayed to the user.
# """
# pass
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/openid/server/trustroot.py 0000644 0001750 0001750 00000034127 00000000000 021531 0 ustar 00rami rami 0000000 0000000 # -*- test-case-name: openid.test.test_rpverify -*-
"""
This module contains the C{L{TrustRoot}} class, which helps handle
trust root checking. This module is used by the
C{L{openid.server.server}} module, but it is also available to server
implementers who wish to use it for additional trust root checking.
It also implements relying party return_to URL verification, based on
the realm.
"""
__all__ = [
'TrustRoot',
'RP_RETURN_TO_URL_TYPE',
'extractReturnToURLs',
'returnToMatches',
'verifyReturnTo',
]
from openid import urinorm
from openid.yadis import services
from urllib.parse import urlparse, urlunparse
import re
import logging
logger = logging.getLogger(__name__)
############################################
_protocols = ['http', 'https']
_top_level_domains = [
'ac', 'ad', 'ae', 'aero', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq',
'ar', 'arpa', 'as', 'asia', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
'be', 'bf', 'bg', 'bh', 'bi', 'biz', 'bj', 'bm', 'bn', 'bo', 'br', 'bs',
'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cat', 'cc', 'cd', 'cf', 'cg', 'ch',
'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'com', 'coop', 'cr', 'cu', 'cv', 'cx',
'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'edu', 'ee', 'eg',
'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb',
'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gov', 'gp', 'gq',
'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
'id', 'ie', 'il', 'im', 'in', 'info', 'int', 'io', 'iq', 'ir', 'is', 'it',
'je', 'jm', 'jo', 'jobs', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp',
'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt',
'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mil', 'mk', 'ml',
'mm', 'mn', 'mo', 'mobi', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum',
'mv', 'mw', 'mx', 'my', 'mz', 'na', 'name', 'nc', 'ne', 'net', 'nf', 'ng',
'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'org', 'pa', 'pe', 'pf',
'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'pro', 'ps', 'pt', 'pw', 'py',
'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg',
'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'su', 'sv',
'sy', 'sz', 'tc', 'td', 'tel', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm',
'tn', 'to', 'tp', 'tr', 'travel', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk',
'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws',
'xn--0zwm56d', 'xn--11b5bs3a9aj6g', 'xn--80akhbyknj4f', 'xn--9t4b11yi5a',
'xn--deba0ad', 'xn--g6w251d', 'xn--hgbk6aj7f53bba', 'xn--hlcj6aya9esc7a',
'xn--jxalpdlp', 'xn--kgbechtv', 'xn--zckzah', 'ye', 'yt', 'yu', 'za', 'zm',
'zw'
]
# Build from RFC3986, section 3.2.2. Used to reject hosts with invalid
# characters.
host_segment_re = re.compile(
r"(?:[-a-zA-Z0-9!$&'\(\)\*+,;=._~]|%[a-zA-Z0-9]{2})+$")
class RealmVerificationRedirected(Exception):
"""Attempting to verify this realm resulted in a redirect.
@since: 2.1.0
"""
def __init__(self, relying_party_url, rp_url_after_redirects):
self.relying_party_url = relying_party_url
self.rp_url_after_redirects = rp_url_after_redirects
def __str__(self):
return ("Attempting to verify %r resulted in "
"redirect to %r" % (self.relying_party_url,
self.rp_url_after_redirects))
def _parseURL(url):
try:
url = urinorm.urinorm(url)
except ValueError:
return None
proto, netloc, path, params, query, frag = urlparse(url)
if not path:
# Python <2.4 does not parse URLs with no path properly
if not query and '?' in netloc:
netloc, query = netloc.split('?', 1)
path = '/'
path = urlunparse(('', '', path, params, query, frag))
if ':' in netloc:
try:
host, port = netloc.split(':')
except ValueError:
return None
if not re.match(r'\d+$', port):
return None
else:
host = netloc
port = ''
host = host.lower()
if not host_segment_re.match(host):
return None
return proto, host, port, path
class TrustRoot(object):
"""
This class represents an OpenID trust root. The C{L{parse}}
classmethod accepts a trust root string, producing a
C{L{TrustRoot}} object. The method OpenID server implementers
would be most likely to use is the C{L{isSane}} method, which
checks the trust root for given patterns that indicate that the
trust root is too broad or points to a local network resource.
@sort: parse, isSane
"""
def __init__(self, unparsed, proto, wildcard, host, port, path):
self.unparsed = unparsed
self.proto = proto
self.wildcard = wildcard
self.host = host
self.port = port
self.path = path
def isSane(self):
"""
This method checks the to see if a trust root represents a
reasonable (sane) set of URLs. 'http://*.com/', for example
is not a reasonable pattern, as it cannot meaningfully specify
the site claiming it. This function attempts to find many
related examples, but it can only work via heuristics.
Negative responses from this method should be treated as
advisory, used only to alert the user to examine the trust
root carefully.
@return: Whether the trust root is sane
@rtype: C{bool}
"""
if self.host == 'localhost':
return True
host_parts = self.host.split('.')
if self.wildcard:
assert host_parts[0] == '', host_parts
del host_parts[0]
# If it's an absolute domain name, remove the empty string
# from the end.
if host_parts and not host_parts[-1]:
del host_parts[-1]
if not host_parts:
return False
# Do not allow adjacent dots
if '' in host_parts:
return False
tld = host_parts[-1]
if tld not in _top_level_domains:
return False
if len(host_parts) == 1:
return False
if self.wildcard:
if len(tld) == 2 and len(host_parts[-2]) <= 3:
# It's a 2-letter tld with a short second to last segment
# so there needs to be more than two segments specified
# (e.g. *.co.uk is insane)
return len(host_parts) > 2
# Passed all tests for insanity.
return True
def validateURL(self, url):
"""
Validates a URL against this trust root.
@param url: The URL to check
@type url: C{str}
@return: Whether the given URL is within this trust root.
@rtype: C{bool}
"""
url_parts = _parseURL(url)
if url_parts is None:
return False
proto, host, port, path = url_parts
if proto != self.proto:
return False
if port != self.port:
return False
if '*' in host:
return False
if not self.wildcard:
if host != self.host:
return False
elif ((not host.endswith(self.host)) and ('.' + host) != self.host):
return False
if path != self.path:
path_len = len(self.path)
trust_prefix = self.path[:path_len]
url_prefix = path[:path_len]
# must be equal up to the length of the path, at least
if trust_prefix != url_prefix:
return False
# These characters must be on the boundary between the end
# of the trust root's path and the start of the URL's
# path.
if '?' in self.path:
allowed = '&'
else:
allowed = '?/'
return (self.path[-1] in allowed or path[path_len] in allowed)
return True
def parse(cls, trust_root):
"""
This method creates a C{L{TrustRoot}} instance from the given
input, if possible.
@param trust_root: This is the trust root to parse into a
C{L{TrustRoot}} object.
@type trust_root: C{str}
@return: A C{L{TrustRoot}} instance if trust_root parses as a
trust root, C{None} otherwise.
@rtype: C{NoneType} or C{L{TrustRoot}}
"""
url_parts = _parseURL(trust_root)
if url_parts is None:
return None
proto, host, port, path = url_parts
# check for valid prototype
if proto not in _protocols:
return None
# check for URI fragment
if path.find('#') != -1:
return None
# extract wildcard if it is there
if host.find('*', 1) != -1:
# wildcard must be at start of domain: *.foo.com, not foo.*.com
return None
if host.startswith('*'):
# Starts with star, so must have a dot after it (if a
# domain is specified)
if len(host) > 1 and host[1] != '.':
return None
host = host[1:]
wilcard = True
else:
wilcard = False
# we have a valid trust root
tr = cls(trust_root, proto, wilcard, host, port, path)
return tr
parse = classmethod(parse)
def checkSanity(cls, trust_root_string):
"""str -> bool
is this a sane trust root?
"""
trust_root = cls.parse(trust_root_string)
if trust_root is None:
return False
else:
return trust_root.isSane()
checkSanity = classmethod(checkSanity)
def checkURL(cls, trust_root, url):
"""quick func for validating a url against a trust root. See the
TrustRoot class if you need more control."""
tr = cls.parse(trust_root)
return tr is not None and tr.validateURL(url)
checkURL = classmethod(checkURL)
def buildDiscoveryURL(self):
"""Return a discovery URL for this realm.
This function does not check to make sure that the realm is
valid. Its behaviour on invalid inputs is undefined.
@rtype: str
@returns: The URL upon which relying party discovery should be run
in order to verify the return_to URL
@since: 2.1.0
"""
if self.wildcard:
# Use "www." in place of the star
assert self.host.startswith('.'), self.host
www_domain = 'www' + self.host
return '%s://%s%s' % (self.proto, www_domain, self.path)
else:
return self.unparsed
def __repr__(self):
return "TrustRoot(%r, %r, %r, %r, %r, %r)" % (
self.unparsed, self.proto, self.wildcard, self.host, self.port,
self.path)
def __str__(self):
return repr(self)
# The URI for relying party discovery, used in realm verification.
#
# XXX: This should probably live somewhere else (like in
# openid.consumer or openid.yadis somewhere)
RP_RETURN_TO_URL_TYPE = 'http://specs.openid.net/auth/2.0/return_to'
def _extractReturnURL(endpoint):
"""If the endpoint is a relying party OpenID return_to endpoint,
return the endpoint URL. Otherwise, return None.
This function is intended to be used as a filter for the Yadis
filtering interface.
@see: C{L{openid.yadis.services}}
@see: C{L{openid.yadis.filters}}
@param endpoint: An XRDS BasicServiceEndpoint, as returned by
performing Yadis dicovery.
@returns: The endpoint URL or None if the endpoint is not a
relying party endpoint.
@rtype: str or NoneType
"""
if endpoint.matchTypes([RP_RETURN_TO_URL_TYPE]):
return endpoint.uri
else:
return None
def returnToMatches(allowed_return_to_urls, return_to):
"""Is the return_to URL under one of the supplied allowed
return_to URLs?
@since: 2.1.0
"""
for allowed_return_to in allowed_return_to_urls:
# A return_to pattern works the same as a realm, except that
# it's not allowed to use a wildcard. We'll model this by
# parsing it as a realm, and not trying to match it if it has
# a wildcard.
return_realm = TrustRoot.parse(allowed_return_to)
if ( # Parses as a trust root
return_realm is not None and
# Does not have a wildcard
not return_realm.wildcard and
# Matches the return_to that we passed in with it
return_realm.validateURL(return_to)):
return True
# No URL in the list matched
return False
def getAllowedReturnURLs(relying_party_url):
"""Given a relying party discovery URL return a list of return_to URLs.
@since: 2.1.0
"""
(rp_url_after_redirects, return_to_urls) = services.getServiceEndpoints(
relying_party_url, _extractReturnURL)
if rp_url_after_redirects != relying_party_url:
# Verification caused a redirect
raise RealmVerificationRedirected(relying_party_url,
rp_url_after_redirects)
return return_to_urls
# _vrfy parameter is there to make testing easier
def verifyReturnTo(realm_str, return_to, _vrfy=getAllowedReturnURLs):
"""Verify that a return_to URL is valid for the given realm.
This function builds a discovery URL, performs Yadis discovery on
it, makes sure that the URL does not redirect, parses out the
return_to URLs, and finally checks to see if the current return_to
URL matches the return_to.
@raises DiscoveryFailure: When Yadis discovery fails
@returns: True if the return_to URL is valid for the realm
@since: 2.1.0
"""
realm = TrustRoot.parse(realm_str)
if realm is None:
# The realm does not parse as a URL pattern
return False
try:
allowable_urls = _vrfy(realm.buildDiscoveryURL())
except RealmVerificationRedirected as err:
logger.exception(str(err))
return False
if returnToMatches(allowable_urls, return_to):
return True
else:
logger.error("Failed to validate return_to %r for realm %r, was not "
"in %s" % (return_to, realm_str, allowable_urls))
return False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/sreg.py 0000644 0001750 0001750 00000000303 00000000000 017063 0 ustar 00rami rami 0000000 0000000 """moved to L{openid.extensions.sreg}"""
import warnings
warnings.warn("openid.sreg has moved to openid.extensions.sreg",
DeprecationWarning)
from openid.extensions.sreg import *
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 011452 x ustar 00 0000000 0000000 28 mtime=1593432941.8718696
python3-openid-3.2.0/openid/store/ 0000755 0001750 0001750 00000000000 00000000000 016711 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/store/__init__.py 0000644 0001750 0001750 00000000327 00000000000 021024 0 ustar 00rami rami 0000000 0000000 """
This package contains the modules related to this library's use of
persistent storage.
@sort: interface, filestore, sqlstore, memstore
"""
__all__ = ['interface', 'filestore', 'sqlstore', 'memstore', 'nonce']
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586713681.0
python3-openid-3.2.0/openid/store/filestore.py 0000644 0001750 0001750 00000030601 00000000000 021257 0 ustar 00rami rami 0000000 0000000 """
This module contains an C{L{OpenIDStore}} implementation backed by
flat files.
"""
import string
import os
import os.path
import time
import logging
from errno import EEXIST, ENOENT
from tempfile import mkstemp
from openid.association import Association
from openid.store.interface import OpenIDStore
from openid.store import nonce
from openid import cryptutil, oidutil
logger = logging.getLogger(__name__)
_filename_allowed = string.ascii_letters + string.digits + '.'
_isFilenameSafe = set(_filename_allowed).__contains__
def _safe64(s):
h64 = oidutil.toBase64(cryptutil.sha1(s))
# to be able to manipulate it, make it a bytearray
h64 = bytearray(h64)
h64 = h64.replace(b'+', b'_')
h64 = h64.replace(b'/', b'.')
h64 = h64.replace(b'=', b'')
return bytes(h64)
def _filenameEscape(s):
filename_chunks = []
for c in s:
if _isFilenameSafe(c):
filename_chunks.append(c)
else:
filename_chunks.append('_%02X' % ord(c))
return ''.join(filename_chunks)
def _removeIfPresent(filename):
"""Attempt to remove a file, returning whether the file existed at
the time of the call.
str -> bool
"""
try:
os.unlink(filename)
except OSError as why:
if why.errno == ENOENT:
# Someone beat us to it, but it's gone, so that's OK
return 0
else:
raise
else:
# File was present
return 1
def _ensureDir(dir_name):
"""Create dir_name as a directory if it does not exist. If it
exists, make sure that it is, in fact, a directory.
Can raise OSError
str -> NoneType
"""
try:
os.makedirs(dir_name)
except OSError as why:
if why.errno != EEXIST or not os.path.isdir(dir_name):
raise
class FileOpenIDStore(OpenIDStore):
"""
This is a filesystem-based store for OpenID associations and
nonces. This store should be safe for use in concurrent systems
on both windows and unix (excluding NFS filesystems). There are a
couple race conditions in the system, but those failure cases have
been set up in such a way that the worst-case behavior is someone
having to try to log in a second time.
Most of the methods of this class are implementation details.
People wishing to just use this store need only pay attention to
the C{L{__init__}} method.
Methods of this object can raise OSError if unexpected filesystem
conditions, such as bad permissions or missing directories, occur.
"""
def __init__(self, directory):
"""
Initializes a new FileOpenIDStore. This initializes the
nonce and association directories, which are subdirectories of
the directory passed in.
@param directory: This is the directory to put the store
directories in.
@type directory: C{str}
"""
# Make absolute
directory = os.path.normpath(os.path.abspath(directory))
self.nonce_dir = os.path.join(directory, 'nonces')
self.association_dir = os.path.join(directory, 'associations')
# Temp dir must be on the same filesystem as the assciations
# directory
self.temp_dir = os.path.join(directory, 'temp')
self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds
self._setup()
def _setup(self):
"""Make sure that the directories in which we store our data
exist.
() -> NoneType
"""
_ensureDir(self.nonce_dir)
_ensureDir(self.association_dir)
_ensureDir(self.temp_dir)
def _mktemp(self):
"""Create a temporary file on the same filesystem as
self.association_dir.
The temporary directory should not be cleaned if there are any
processes using the store. If there is no active process using
the store, it is safe to remove all of the files in the
temporary directory.
() -> (file, str)
"""
fd, name = mkstemp(dir=self.temp_dir)
try:
file_obj = os.fdopen(fd, 'wb')
return file_obj, name
except:
_removeIfPresent(name)
raise
def getAssociationFilename(self, server_url, handle):
"""Create a unique filename for a given server url and
handle. This implementation does not assume anything about the
format of the handle. The filename that is returned will
contain the domain name from the server URL for ease of human
inspection of the data directory.
(str, str) -> str
"""
if server_url.find('://') == -1:
raise ValueError('Bad server URL: %r' % server_url)
proto, rest = server_url.split('://', 1)
domain = _filenameEscape(rest.split('/', 1)[0])
url_hash = _safe64(server_url)
if handle:
handle_hash = _safe64(handle)
else:
handle_hash = ''
filename = '%s-%s-%s-%s' % (proto, domain, url_hash, handle_hash)
return os.path.join(self.association_dir, filename)
def storeAssociation(self, server_url, association):
"""Store an association in the association directory.
(str, Association) -> NoneType
"""
association_s = association.serialize() # NOTE: UTF-8 encoded bytes
filename = self.getAssociationFilename(server_url, association.handle)
tmp_file, tmp = self._mktemp()
try:
try:
tmp_file.write(association_s)
os.fsync(tmp_file.fileno())
finally:
tmp_file.close()
try:
os.rename(tmp, filename)
except OSError as why:
if why.errno != EEXIST:
raise
# We only expect EEXIST to happen only on Windows. It's
# possible that we will succeed in unlinking the existing
# file, but not in putting the temporary file in place.
try:
os.unlink(filename)
except OSError as why:
if why.errno == ENOENT:
pass
else:
raise
# Now the target should not exist. Try renaming again,
# giving up if it fails.
os.rename(tmp, filename)
except:
# If there was an error, don't leave the temporary file
# around.
_removeIfPresent(tmp)
raise
def getAssociation(self, server_url, handle=None):
"""Retrieve an association. If no handle is specified, return
the association with the latest expiration.
(str, str or NoneType) -> Association or NoneType
"""
if handle is None:
handle = ''
# The filename with the empty handle is a prefix of all other
# associations for the given server URL.
filename = self.getAssociationFilename(server_url, handle)
if handle:
return self._getAssociation(filename)
else:
association_files = os.listdir(self.association_dir)
matching_files = []
# strip off the path to do the comparison
name = os.path.basename(filename)
for association_file in association_files:
if association_file.startswith(name):
matching_files.append(association_file)
matching_associations = []
# read the matching files and sort by time issued
for name in matching_files:
full_name = os.path.join(self.association_dir, name)
association = self._getAssociation(full_name)
if association is not None:
matching_associations.append(
(association.issued, association))
matching_associations.sort()
# return the most recently issued one.
if matching_associations:
(_, assoc) = matching_associations[-1]
return assoc
else:
return None
def _getAssociation(self, filename):
try:
assoc_file = open(filename, 'rb')
except IOError as why:
if why.errno == ENOENT:
# No association exists for that URL and handle
return None
else:
raise
try:
assoc_s = assoc_file.read()
finally:
assoc_file.close()
try:
association = Association.deserialize(assoc_s)
except ValueError:
_removeIfPresent(filename)
return None
# Clean up expired associations
if association.expiresIn == 0:
_removeIfPresent(filename)
return None
else:
return association
def removeAssociation(self, server_url, handle):
"""Remove an association if it exists. Do nothing if it does not.
(str, str) -> bool
"""
assoc = self.getAssociation(server_url, handle)
if assoc is None:
return 0
else:
filename = self.getAssociationFilename(server_url, handle)
return _removeIfPresent(filename)
def useNonce(self, server_url, timestamp, salt):
"""Return whether this nonce is valid.
str -> bool
"""
if abs(timestamp - time.time()) > nonce.SKEW:
return False
if server_url:
proto, rest = server_url.split('://', 1)
else:
# Create empty proto / rest values for empty server_url,
# which is part of a consumer-generated nonce.
proto, rest = '', ''
domain = _filenameEscape(rest.split('/', 1)[0])
url_hash = _safe64(server_url)
salt_hash = _safe64(salt)
filename = '%08x-%s-%s-%s-%s' % (timestamp, proto, domain, url_hash,
salt_hash)
filename = os.path.join(self.nonce_dir, filename)
try:
fd = os.open(filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o200)
except OSError as why:
if why.errno == EEXIST:
return False
else:
raise
else:
os.close(fd)
return True
def _allAssocs(self):
all_associations = []
association_filenames = [
os.path.join(self.association_dir, filename)
for filename in os.listdir(self.association_dir)
]
for association_filename in association_filenames:
try:
association_file = open(association_filename, 'rb')
except IOError as why:
if why.errno == ENOENT:
logger.exception("%s disappeared during %s._allAssocs" % (
association_filename, self.__class__.__name__))
else:
raise
else:
try:
assoc_s = association_file.read()
finally:
association_file.close()
# Remove expired or corrupted associations
try:
association = Association.deserialize(assoc_s)
except ValueError:
_removeIfPresent(association_filename)
else:
all_associations.append(
(association_filename, association))
return all_associations
def cleanup(self):
"""Remove expired entries from the database. This is
potentially expensive, so only run when it is acceptable to
take time.
() -> NoneType
"""
self.cleanupAssociations()
self.cleanupNonces()
def cleanupAssociations(self):
removed = 0
for assoc_filename, assoc in self._allAssocs():
if assoc.expiresIn == 0:
_removeIfPresent(assoc_filename)
removed += 1
return removed
def cleanupNonces(self):
nonces = os.listdir(self.nonce_dir)
now = time.time()
removed = 0
# Check all nonces for expiry
for nonce_fname in nonces:
timestamp = nonce_fname.split('-', 1)[0]
timestamp = int(timestamp, 16)
if abs(timestamp - now) > nonce.SKEW:
filename = os.path.join(self.nonce_dir, nonce_fname)
_removeIfPresent(filename)
removed += 1
return removed
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/store/interface.py 0000644 0001750 0001750 00000015654 00000000000 021236 0 ustar 00rami rami 0000000 0000000 """
This module contains the definition of the C{L{OpenIDStore}}
interface.
"""
class OpenIDStore(object):
"""
This is the interface for the store objects the OpenID library
uses. It is a single class that provides all of the persistence
mechanisms that the OpenID library needs, for both servers and
consumers.
@change: Version 2.0 removed the C{storeNonce}, C{getAuthKey}, and C{isDumb}
methods, and changed the behavior of the C{L{useNonce}} method
to support one-way nonces. It added C{L{cleanupNonces}},
C{L{cleanupAssociations}}, and C{L{cleanup}}.
@sort: storeAssociation, getAssociation, removeAssociation,
useNonce
"""
def storeAssociation(self, server_url, association):
"""
This method puts a C{L{Association
}} object into storage,
retrievable by server URL and handle.
@param server_url: The URL of the identity server that this
association is with. Because of the way the server
portion of the library uses this interface, don't assume
there are any limitations on the character set of the
input string. In particular, expect to see unescaped
non-url-safe characters in the server_url field.
@type server_url: C{str}
@param association: The C{L{Association
}} to store.
@type association: C{L{Association
}}
@return: C{None}
@rtype: C{NoneType}
"""
raise NotImplementedError
def getAssociation(self, server_url, handle=None):
"""
This method returns an C{L{Association
}} object from storage that
matches the server URL and, if specified, handle. It returns
C{None} if no such association is found or if the matching
association is expired.
If no handle is specified, the store may return any
association which matches the server URL. If multiple
associations are valid, the recommended return value for this
method is the one most recently issued.
This method is allowed (and encouraged) to garbage collect
expired associations when found. This method must not return
expired associations.
@param server_url: The URL of the identity server to get the
association for. Because of the way the server portion of
the library uses this interface, don't assume there are
any limitations on the character set of the input string.
In particular, expect to see unescaped non-url-safe
characters in the server_url field.
@type server_url: C{str}
@param handle: This optional parameter is the handle of the
specific association to get. If no specific handle is
provided, any valid association matching the server URL is
returned.
@type handle: C{str} or C{NoneType}
@return: The C{L{Association
}} for the given identity
server.
@rtype: C{L{Association }} or
C{NoneType}
"""
raise NotImplementedError
def removeAssociation(self, server_url, handle):
"""
This method removes the matching association if it's found,
and returns whether the association was removed or not.
@param server_url: The URL of the identity server the
association to remove belongs to. Because of the way the
server portion of the library uses this interface, don't
assume there are any limitations on the character set of
the input string. In particular, expect to see unescaped
non-url-safe characters in the server_url field.
@type server_url: C{str}
@param handle: This is the handle of the association to
remove. If there isn't an association found that matches
both the given URL and handle, then there was no matching
handle found.
@type handle: C{str}
@return: Returns whether or not the given association existed.
@rtype: C{bool} or C{int}
"""
raise NotImplementedError
def useNonce(self, server_url, timestamp, salt):
"""Called when using a nonce.
This method should return C{True} if the nonce has not been
used before, and store it for a while to make sure nobody
tries to use the same value again. If the nonce has already
been used or the timestamp is not current, return C{False}.
You may use L{openid.store.nonce.SKEW} for your timestamp window.
@change: In earlier versions, round-trip nonces were used and
a nonce was only valid if it had been previously stored
with C{storeNonce}. Version 2.0 uses one-way nonces,
requiring a different implementation here that does not
depend on a C{storeNonce} call. (C{storeNonce} is no
longer part of the interface.)
@param server_url: The URL of the server from which the nonce
originated.
@type server_url: C{str}
@param timestamp: The time that the nonce was created (to the
nearest second), in seconds since January 1 1970 UTC.
@type timestamp: C{int}
@param salt: A random string that makes two nonces from the
same server issued during the same second unique.
@type salt: str
@return: Whether or not the nonce was valid.
@rtype: C{bool}
"""
raise NotImplementedError
def cleanupNonces(self):
"""Remove expired nonces from the store.
Discards any nonce from storage that is old enough that its
timestamp would not pass L{useNonce}.
This method is not called in the normal operation of the
library. It provides a way for store admins to keep
their storage from filling up with expired data.
@return: the number of nonces expired.
@returntype: int
"""
raise NotImplementedError
def cleanupAssociations(self):
"""Remove expired associations from the store.
This method is not called in the normal operation of the
library. It provides a way for store admins to keep
their storage from filling up with expired data.
@return: the number of associations expired.
@returntype: int
"""
raise NotImplementedError
def cleanup(self):
"""Shortcut for C{L{cleanupNonces}()}, C{L{cleanupAssociations}()}.
This method is not called in the normal operation of the
library. It provides a way for store admins to keep
their storage from filling up with expired data.
"""
return self.cleanupNonces(), self.cleanupAssociations()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/store/memstore.py 0000644 0001750 0001750 00000007003 00000000000 021116 0 ustar 00rami rami 0000000 0000000 """A simple store using only in-process memory."""
from openid.store import nonce
import copy
import time
class ServerAssocs(object):
def __init__(self):
self.assocs = {}
def set(self, assoc):
self.assocs[assoc.handle] = assoc
def get(self, handle):
return self.assocs.get(handle)
def remove(self, handle):
try:
del self.assocs[handle]
except KeyError:
return False
else:
return True
def best(self):
"""Returns association with the oldest issued date.
or None if there are no associations.
"""
best = None
for assoc in list(self.assocs.values()):
if best is None or best.issued < assoc.issued:
best = assoc
return best
def cleanup(self):
"""Remove expired associations.
@return: tuple of (removed associations, remaining associations)
"""
remove = []
for handle, assoc in self.assocs.items():
if assoc.expiresIn == 0:
remove.append(handle)
for handle in remove:
del self.assocs[handle]
return len(remove), len(self.assocs)
class MemoryStore(object):
"""In-process memory store.
Use for single long-running processes. No persistence supplied.
"""
def __init__(self):
self.server_assocs = {}
self.nonces = {}
def _getServerAssocs(self, server_url):
try:
return self.server_assocs[server_url]
except KeyError:
assocs = self.server_assocs[server_url] = ServerAssocs()
return assocs
def storeAssociation(self, server_url, assoc):
assocs = self._getServerAssocs(server_url)
assocs.set(copy.deepcopy(assoc))
def getAssociation(self, server_url, handle=None):
assocs = self._getServerAssocs(server_url)
if handle is None:
return assocs.best()
else:
return assocs.get(handle)
def removeAssociation(self, server_url, handle):
assocs = self._getServerAssocs(server_url)
return assocs.remove(handle)
def useNonce(self, server_url, timestamp, salt):
if abs(timestamp - time.time()) > nonce.SKEW:
return False
anonce = (str(server_url), int(timestamp), str(salt))
if anonce in self.nonces:
return False
else:
self.nonces[anonce] = None
return True
def cleanupNonces(self):
now = time.time()
expired = []
for anonce in self.nonces.keys():
if abs(anonce[1] - now) > nonce.SKEW:
# removing items while iterating over the set could be bad.
expired.append(anonce)
for anonce in expired:
del self.nonces[anonce]
return len(expired)
def cleanupAssociations(self):
remove_urls = []
removed_assocs = 0
for server_url, assocs in self.server_assocs.items():
removed, remaining = assocs.cleanup()
removed_assocs += removed
if not remaining:
remove_urls.append(server_url)
# Remove entries from server_assocs that had none remaining.
for server_url in remove_urls:
del self.server_assocs[server_url]
return removed_assocs
def __eq__(self, other):
return ((self.server_assocs == other.server_assocs) and
(self.nonces == other.nonces))
def __ne__(self, other):
return not (self == other)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/store/nonce.py 0000644 0001750 0001750 00000005433 00000000000 020372 0 ustar 00rami rami 0000000 0000000 __all__ = [
'split',
'mkNonce',
'checkTimestamp',
]
from openid import cryptutil
from time import strptime, strftime, gmtime, time
from calendar import timegm
import string
NONCE_CHARS = string.ascii_letters + string.digits
# Keep nonces for five hours (allow five hours for the combination of
# request time and clock skew). This is probably way more than is
# necessary, but there is not much overhead in storing nonces.
SKEW = 60 * 60 * 5
time_fmt = '%Y-%m-%dT%H:%M:%SZ'
time_str_len = len('0000-00-00T00:00:00Z')
def split(nonce_string):
"""Extract a timestamp from the given nonce string
@param nonce_string: the nonce from which to extract the timestamp
@type nonce_string: str
@returns: A pair of a Unix timestamp and the salt characters
@returntype: (int, str)
@raises ValueError: if the nonce does not start with a correctly
formatted time string
"""
timestamp_str = nonce_string[:time_str_len]
try:
timestamp = timegm(strptime(timestamp_str, time_fmt))
except AssertionError: # Python 2.2
timestamp = -1
if timestamp < 0:
raise ValueError('time out of range')
return timestamp, nonce_string[time_str_len:]
def checkTimestamp(nonce_string, allowed_skew=SKEW, now=None):
"""Is the timestamp that is part of the specified nonce string
within the allowed clock-skew of the current time?
@param nonce_string: The nonce that is being checked
@type nonce_string: str
@param allowed_skew: How many seconds should be allowed for
completing the request, allowing for clock skew.
@type allowed_skew: int
@param now: The current time, as a Unix timestamp
@type now: int
@returntype: bool
@returns: Whether the timestamp is correctly formatted and within
the allowed skew of the current time.
"""
try:
stamp, _ = split(nonce_string)
except ValueError:
return False
else:
if now is None:
now = time()
# Time after which we should not use the nonce
past = now - allowed_skew
# Time that is too far in the future for us to allow
future = now + allowed_skew
# the stamp is not too far in the future and is not too far in
# the past
return past <= stamp <= future
def mkNonce(when=None):
"""Generate a nonce with the current timestamp
@param when: Unix timestamp representing the issue time of the
nonce. Defaults to the current time.
@type when: int
@returntype: str
@returns: A string that should be usable as a one-way nonce
@see: time
"""
salt = cryptutil.randomString(6, NONCE_CHARS)
if when is None:
t = gmtime()
else:
t = gmtime(when)
time_str = strftime(time_fmt, t)
return time_str + salt
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586712376.0
python3-openid-3.2.0/openid/store/sqlstore.py 0000644 0001750 0001750 00000040414 00000000000 021142 0 ustar 00rami rami 0000000 0000000 """
This module contains C{L{OpenIDStore}} implementations that use
various SQL databases to back them.
Example of how to initialize a store database::
python -c 'from openid.store import sqlstore; import pysqlite2.dbapi2;'
'sqlstore.SQLiteStore(pysqlite2.dbapi2.connect("cstore.db")).createTables()'
"""
import re
import time
from openid.association import Association
from openid.store.interface import OpenIDStore
from openid.store import nonce
def _inTxn(func):
def wrapped(self, *args, **kwargs):
return self._callInTransaction(func, self, *args, **kwargs)
if hasattr(func, '__name__'):
try:
wrapped.__name__ = func.__name__[4:]
except TypeError:
pass
if hasattr(func, '__doc__'):
wrapped.__doc__ = func.__doc__
return wrapped
class SQLStore(OpenIDStore):
"""
This is the parent class for the SQL stores, which contains the
logic common to all of the SQL stores.
The table names used are determined by the class variables
C{L{associations_table}} and
C{L{nonces_table}}. To change the name of the tables used, pass
new table names into the constructor.
To create the tables with the proper schema, see the
C{L{createTables}} method.
This class shouldn't be used directly. Use one of its subclasses
instead, as those contain the code necessary to use a specific
database.
All methods other than C{L{__init__}} and C{L{createTables}}
should be considered implementation details.
@cvar associations_table: This is the default name of the table to
keep associations in
@cvar nonces_table: This is the default name of the table to keep
nonces in.
@sort: __init__, createTables
"""
associations_table = 'oid_associations'
nonces_table = 'oid_nonces'
def __init__(self, conn, associations_table=None, nonces_table=None):
"""
This creates a new SQLStore instance. It requires an
established database connection be given to it, and it allows
overriding the default table names.
@param conn: This must be an established connection to a
database of the correct type for the SQLStore subclass
you're using.
@type conn: A python database API compatible connection
object.
@param associations_table: This is an optional parameter to
specify the name of the table used for storing
associations. The default value is specified in
C{L{SQLStore.associations_table}}.
@type associations_table: C{str}
@param nonces_table: This is an optional parameter to specify
the name of the table used for storing nonces. The
default value is specified in C{L{SQLStore.nonces_table}}.
@type nonces_table: C{str}
"""
self.conn = conn
self.cur = None
self._statement_cache = {}
self._table_names = {
'associations': associations_table or self.associations_table,
'nonces': nonces_table or self.nonces_table,
}
self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds
# DB API extension: search for "Connection Attributes .Error,
# .ProgrammingError, etc." in
# http://www.python.org/dev/peps/pep-0249/
if (hasattr(self.conn, 'IntegrityError') and
hasattr(self.conn, 'OperationalError')):
self.exceptions = self.conn
if not (hasattr(self.exceptions, 'IntegrityError') and
hasattr(self.exceptions, 'OperationalError')):
raise RuntimeError("Error using database connection module "
"(Maybe it can't be imported?)")
def blobDecode(self, blob):
"""Convert a blob as returned by the SQL engine into a str object.
str -> str"""
return blob
def blobEncode(self, s):
"""Convert a str object into the necessary object for storing
in the database as a blob."""
return s
def _getSQL(self, sql_name):
try:
return self._statement_cache[sql_name]
except KeyError:
sql = getattr(self, sql_name)
sql %= self._table_names
self._statement_cache[sql_name] = sql
return sql
def _execSQL(self, sql_name, *args):
sql = self._getSQL(sql_name)
# Kludge because we have reports of postgresql not quoting
# arguments if they are passed in as unicode instead of str.
# Currently the strings in our tables just have ascii in them,
# so this ought to be safe.
def unicode_to_str(arg):
if isinstance(arg, str):
return str(arg)
else:
return arg
str_args = list(map(unicode_to_str, args))
self.cur.execute(sql, str_args)
def __getattr__(self, attr):
# if the attribute starts with db_, use a default
# implementation that looks up the appropriate SQL statement
# as an attribute of this object and executes it.
if attr[:3] == 'db_':
sql_name = attr[3:] + '_sql'
def func(*args):
return self._execSQL(sql_name, *args)
setattr(self, attr, func)
return func
else:
raise AttributeError('Attribute %r not found' % (attr, ))
def _callInTransaction(self, func, *args, **kwargs):
"""Execute the given function inside of a transaction, with an
open cursor. If no exception is raised, the transaction is
comitted, otherwise it is rolled back."""
# No nesting of transactions
self.conn.rollback()
try:
self.cur = self.conn.cursor()
try:
ret = func(*args, **kwargs)
finally:
self.cur.close()
self.cur = None
except:
self.conn.rollback()
raise
else:
self.conn.commit()
return ret
def txn_createTables(self):
"""
This method creates the database tables necessary for this
store to work. It should not be called if the tables already
exist.
"""
self.db_create_nonce()
self.db_create_assoc()
createTables = _inTxn(txn_createTables)
def txn_storeAssociation(self, server_url, association):
"""Set the association for the server URL.
Association -> NoneType
"""
a = association
self.db_set_assoc(server_url, a.handle,
self.blobEncode(a.secret), a.issued, a.lifetime,
a.assoc_type)
storeAssociation = _inTxn(txn_storeAssociation)
def txn_getAssociation(self, server_url, handle=None):
"""Get the most recent association that has been set for this
server URL and handle.
str -> NoneType or Association
"""
if handle is not None:
self.db_get_assoc(server_url, handle)
else:
self.db_get_assocs(server_url)
rows = self.cur.fetchall()
if len(rows) == 0:
return None
else:
associations = []
for values in rows:
values = list(values)
values[1] = self.blobDecode(values[1])
assoc = Association(*values)
if assoc.expiresIn == 0:
self.txn_removeAssociation(server_url, assoc.handle)
else:
associations.append((assoc.issued, assoc))
if associations:
associations.sort()
return associations[-1][1]
else:
return None
getAssociation = _inTxn(txn_getAssociation)
def txn_removeAssociation(self, server_url, handle):
"""Remove the association for the given server URL and handle,
returning whether the association existed at all.
(str, str) -> bool
"""
self.db_remove_assoc(server_url, handle)
return self.cur.rowcount > 0 # -1 is undefined
removeAssociation = _inTxn(txn_removeAssociation)
def txn_useNonce(self, server_url, timestamp, salt):
"""Return whether this nonce is present, and if it is, then
remove it from the set.
str -> bool"""
if abs(timestamp - time.time()) > nonce.SKEW:
return False
try:
self.db_add_nonce(server_url, timestamp, salt)
except self.exceptions.IntegrityError:
# The key uniqueness check failed
return False
else:
# The nonce was successfully added
return True
useNonce = _inTxn(txn_useNonce)
def txn_cleanupNonces(self):
self.db_clean_nonce(int(time.time()) - nonce.SKEW)
return self.cur.rowcount
cleanupNonces = _inTxn(txn_cleanupNonces)
def txn_cleanupAssociations(self):
self.db_clean_assoc(int(time.time()))
return self.cur.rowcount
cleanupAssociations = _inTxn(txn_cleanupAssociations)
class SQLiteStore(SQLStore):
"""
This is an SQLite-based specialization of C{L{SQLStore}}.
To create an instance, see C{L{SQLStore.__init__}}. To create the
tables it will use, see C{L{SQLStore.createTables}}.
All other methods are implementation details.
"""
create_nonce_sql = """
CREATE TABLE %(nonces)s (
server_url VARCHAR,
timestamp INTEGER,
salt CHAR(40),
UNIQUE(server_url, timestamp, salt)
);
"""
create_assoc_sql = """
CREATE TABLE %(associations)s
(
server_url VARCHAR(2047),
handle VARCHAR(255),
secret BLOB(128),
issued INTEGER,
lifetime INTEGER,
assoc_type VARCHAR(64),
PRIMARY KEY (server_url, handle)
);
"""
set_assoc_sql = ('INSERT OR REPLACE INTO %(associations)s '
'(server_url, handle, secret, issued, '
'lifetime, assoc_type) '
'VALUES (?, ?, ?, ?, ?, ?);')
get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type '
'FROM %(associations)s WHERE server_url = ?;')
get_assoc_sql = (
'SELECT handle, secret, issued, lifetime, assoc_type '
'FROM %(associations)s WHERE server_url = ? AND handle = ?;')
get_expired_sql = ('SELECT server_url '
'FROM %(associations)s WHERE issued + lifetime < ?;')
remove_assoc_sql = ('DELETE FROM %(associations)s '
'WHERE server_url = ? AND handle = ?;')
clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < ?;'
add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (?, ?, ?);'
clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < ?;'
def blobEncode(self, s):
return memoryview(s)
def useNonce(self, *args, **kwargs):
# Older versions of the sqlite wrapper do not raise
# IntegrityError as they should, so we have to detect the
# message from the OperationalError.
try:
return super(SQLiteStore, self).useNonce(*args, **kwargs)
except self.exceptions.OperationalError as why:
if re.match('^columns .* are not unique$', str(why)):
return False
else:
raise
class MySQLStore(SQLStore):
"""
This is a MySQL-based specialization of C{L{SQLStore}}.
Uses InnoDB tables for transaction support.
To create an instance, see C{L{SQLStore.__init__}}. To create the
tables it will use, see C{L{SQLStore.createTables}}.
All other methods are implementation details.
"""
try:
import MySQLdb as exceptions
except ImportError:
exceptions = None
create_nonce_sql = """
CREATE TABLE %(nonces)s (
server_url BLOB NOT NULL,
timestamp INTEGER NOT NULL,
salt CHAR(40) NOT NULL,
PRIMARY KEY (server_url(255), timestamp, salt)
)
ENGINE=InnoDB;
"""
create_assoc_sql = """
CREATE TABLE %(associations)s
(
server_url BLOB NOT NULL,
handle VARCHAR(255) NOT NULL,
secret BLOB NOT NULL,
issued INTEGER NOT NULL,
lifetime INTEGER NOT NULL,
assoc_type VARCHAR(64) NOT NULL,
PRIMARY KEY (server_url(255), handle)
)
ENGINE=InnoDB;
"""
set_assoc_sql = ('REPLACE INTO %(associations)s '
'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
' FROM %(associations)s WHERE server_url = %%s;')
get_expired_sql = ('SELECT server_url '
'FROM %(associations)s WHERE issued + lifetime < %%s;')
get_assoc_sql = (
'SELECT handle, secret, issued, lifetime, assoc_type'
' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
remove_assoc_sql = ('DELETE FROM %(associations)s '
'WHERE server_url = %%s AND handle = %%s;')
clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
class PostgreSQLStore(SQLStore):
"""
This is a PostgreSQL-based specialization of C{L{SQLStore}}.
To create an instance, see C{L{SQLStore.__init__}}. To create the
tables it will use, see C{L{SQLStore.createTables}}.
All other methods are implementation details.
"""
try:
import psycopg2
except ImportError:
from psycopg2cffi import compat
compat.register()
exceptions = None
create_nonce_sql = """
CREATE TABLE %(nonces)s (
server_url VARCHAR(2047) NOT NULL,
timestamp INTEGER NOT NULL,
salt CHAR(40) NOT NULL,
PRIMARY KEY (server_url, timestamp, salt)
);
"""
create_assoc_sql = """
CREATE TABLE %(associations)s
(
server_url VARCHAR(2047) NOT NULL,
handle VARCHAR(255) NOT NULL,
secret BYTEA NOT NULL,
issued INTEGER NOT NULL,
lifetime INTEGER NOT NULL,
assoc_type VARCHAR(64) NOT NULL,
PRIMARY KEY (server_url, handle),
CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128)
);
"""
def db_set_assoc(self, server_url, handle, secret, issued, lifetime,
assoc_type):
"""
Set an association. This is implemented as a method because
REPLACE INTO is not supported by PostgreSQL (and is not
standard SQL).
"""
result = self.db_get_assoc(server_url, handle)
rows = self.cur.fetchall()
if len(rows):
# Update the table since this associations already exists.
return self.db_update_assoc(secret, issued, lifetime, assoc_type,
server_url, handle)
else:
# Insert a new record because this association wasn't
# found.
return self.db_new_assoc(server_url, handle, secret, issued,
lifetime, assoc_type)
new_assoc_sql = ('INSERT INTO %(associations)s '
'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);')
update_assoc_sql = ('UPDATE %(associations)s SET '
'secret = %%s, issued = %%s, '
'lifetime = %%s, assoc_type = %%s '
'WHERE server_url = %%s AND handle = %%s;')
get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type'
' FROM %(associations)s WHERE server_url = %%s;')
get_expired_sql = ('SELECT server_url '
'FROM %(associations)s WHERE issued + lifetime < %%s;')
get_assoc_sql = (
'SELECT handle, secret, issued, lifetime, assoc_type'
' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;')
remove_assoc_sql = ('DELETE FROM %(associations)s '
'WHERE server_url = %%s AND handle = %%s;')
clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;'
add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);'
clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
def blobEncode(self, blob):
from psycopg2 import Binary
return Binary(blob)
def blobDecode(self, blob):
return blob.tobytes()
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 011451 x ustar 00 0000000 0000000 27 mtime=1593432941.875203
python3-openid-3.2.0/openid/test/ 0000755 0001750 0001750 00000000000 00000000000 016534 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/test/__init__.py 0000644 0001750 0001750 00000012057 00000000000 020652 0 ustar 00rami rami 0000000 0000000 import sys
import os.path
import warnings
import unittest
def addParentToPath():
"""
Add the parent directory to sys.path to make it importable.
"""
try:
d = os.path.dirname(__file__)
except NameError:
d = os.path.dirname(sys.argv[0])
parent = os.path.normpath(os.path.join(d, '..'))
if parent not in sys.path:
print("adding {} to sys.path".format(parent))
sys.path.insert(0, parent)
def specialCaseTests():
"""
Some modules have an explicit `test` function that collects tests --
collect these together as a suite.
"""
function_test_modules = [
'cryptutil',
'oidutil',
'dh',
]
suite = unittest.TestSuite()
for module_name in function_test_modules:
module_name = 'openid.test.' + module_name
try:
test_mod = __import__(module_name, {}, {}, [None])
except ImportError:
print(('Failed to import test %r' % (module_name, )))
else:
suite.addTest(unittest.FunctionTestCase(test_mod.test))
return suite
def pyUnitTests():
"""
Aggregate unit tests from modules, including a few special cases, and
return a suite.
"""
test_module_names = [
'server',
'consumer',
'message',
'symbol',
'etxrd',
'xri',
'xrires',
'association_response',
'auth_request',
'negotiation',
'verifydisco',
'sreg',
'ax',
'pape',
'pape_draft2',
'pape_draft5',
'rpverify',
'extension',
'codecutil',
]
test_modules = [
__import__('openid.test.test_{}'.format(name), {}, {}, ['unused'])
for name in test_module_names
]
try:
from openid.test import test_examples
except ImportError:
# This is very likely due to twill being unimportable, since it's
# ancient and unmaintained. Until the examples are reimplemented using
# something else, we just need to skip it
warnings.warn("Could not import twill; skipping test_examples.")
else:
test_modules.append(test_examples)
# Some modules have data-driven tests, and they use custom methods
# to build the test suite -- the module-level pyUnitTests function should
# return an appropriate test suite
custom_module_names = [
'kvform',
'linkparse',
'oidutil',
'storetest',
'test_accept',
'test_association',
'test_discover',
'test_fetchers',
'test_htmldiscover',
'test_nonce',
'test_openidyadis',
'test_parsehtml',
'test_urinorm',
'test_yadis_discover',
'trustroot',
]
loader = unittest.TestLoader()
suite = unittest.TestSuite()
for m in test_modules:
suite.addTest(loader.loadTestsFromModule(m))
for name in custom_module_names:
mod = __import__('openid.test.{}'.format(name), {}, {}, ['unused'])
try:
suite.addTest(mod.pyUnitTests())
except AttributeError:
# because the AttributeError doesn't actually say which
# object it was.
print(("Error loading tests from %s:" % (name, )))
raise
return suite
def _import_djopenid():
"""
Import djopenid from the examples directory without putting it in sys.path
permanently (which we don't really want to do as we don't want namespace
conflicts)
"""
# Find our way to the examples/djopenid directory
grandParentDir = os.path.join(__file__, "..", "..", "..")
grandParentDir = os.path.abspath(grandParentDir)
examplesDir = os.path.join(grandParentDir, "examples")
sys.path.append(examplesDir)
import djopenid
sys.path.remove(examplesDir)
def djangoExampleTests():
"""
Run tests from examples/djopenid.
@return: number of failed tests.
"""
# Django uses this to find out where its settings are.
os.environ['DJANGO_SETTINGS_MODULE'] = 'djopenid.settings'
_import_djopenid()
try:
import django.test.simple
except ImportError:
raise unittest.SkipTest("Skipping django examples. "
"django.test.simple not found.")
import djopenid.server.models
import djopenid.consumer.models
print("Testing Django examples:")
runner = django.test.simple.DjangoTestSuiteRunner()
return runner.run_tests(['server', 'consumer'])
# These tests do get put into a test suite, so we could run them with the
# other tests, but django also establishes a test database for them, so we
# let it do that thing instead.
return django.test.simple.run_tests(
[djopenid.server.models, djopenid.consumer.models])
def test_suite():
"""
Collect all of the tests together in a single suite.
"""
addParentToPath()
combined_suite = unittest.TestSuite()
combined_suite.addTests(specialCaseTests())
combined_suite.addTests(pyUnitTests())
combined_suite.addTest(unittest.FunctionTestCase(djangoExampleTests))
return combined_suite
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/test/cryptutil.py 0000644 0001750 0001750 00000005700 00000000000 021147 0 ustar 00rami rami 0000000 0000000 import sys
import random
import os.path
from openid import cryptutil
# Most of the purpose of this test is to make sure that cryptutil can
# find a good source of randomness on this machine.
def test_cryptrand():
# It's possible, but HIGHLY unlikely that a correct implementation
# will fail by returning the same number twice
s = cryptutil.getBytes(32)
t = cryptutil.getBytes(32)
assert len(s) == 32
assert len(t) == 32
assert s != t
a = cryptutil.randrange(2**128)
b = cryptutil.randrange(2**128)
assert type(a) is int
assert type(b) is int
assert b != a
# Make sure that we can generate random numbers that are larger
# than platform int size
cryptutil.randrange(int(sys.maxsize) + 1)
def test_reversed():
if hasattr(cryptutil, 'reversed'):
cases = [
('', ''),
('a', 'a'),
('ab', 'ba'),
('abc', 'cba'),
('abcdefg', 'gfedcba'),
([], []),
([1], [1]),
([1, 2], [2, 1]),
([1, 2, 3], [3, 2, 1]),
(list(range(1000)), list(range(999, -1, -1))),
]
for case, expected in cases:
expected = list(expected)
actual = list(cryptutil.reversed(case))
assert actual == expected, (case, expected, actual)
twice = list(cryptutil.reversed(actual))
assert twice == list(case), (actual, case, twice)
def test_binaryLongConvert():
MAX = sys.maxsize
for iteration in range(500):
n = 0
for i in range(10):
n += int(random.randrange(MAX))
s = cryptutil.longToBinary(n)
assert isinstance(s, bytes)
n_prime = cryptutil.binaryToLong(s)
assert n == n_prime, (n, n_prime)
cases = [(b'\x00', 0), (b'\x01', 1), (b'\x7F', 127), (b'\x00\xFF', 255),
(b'\x00\x80', 128), (b'\x00\x81', 129), (b'\x00\x80\x00', 32768),
(b'OpenID is cool', 1611215304203901150134421257416556)]
for s, n in cases:
n_prime = cryptutil.binaryToLong(s)
s_prime = cryptutil.longToBinary(n)
assert n == n_prime, (s, n, n_prime)
assert s == s_prime, (n, s, s_prime)
def test_longToBase64():
f = open(os.path.join(os.path.dirname(__file__), 'n2b64'))
try:
for line in f:
parts = line.strip().split(' ')
p0 = bytes(parts[0], encoding="utf-8")
p1 = cryptutil.longToBase64(int(parts[1]))
assert p0 == p1, (p0, p1, parts)
finally:
f.close()
def test_base64ToLong():
f = open(os.path.join(os.path.dirname(__file__), 'n2b64'))
try:
for line in f:
parts = line.strip().split(' ')
assert int(parts[1]) == cryptutil.base64ToLong(parts[0])
finally:
f.close()
def test():
test_reversed()
test_binaryLongConvert()
test_cryptrand()
test_longToBase64()
test_base64ToLong()
if __name__ == '__main__':
test()
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 011451 x ustar 00 0000000 0000000 27 mtime=1593432941.875203
python3-openid-3.2.0/openid/test/data/ 0000755 0001750 0001750 00000000000 00000000000 017445 5 ustar 00rami rami 0000000 0000000 ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/test/data/accept.txt 0000644 0001750 0001750 00000006262 00000000000 021453 0 ustar 00rami rami 0000000 0000000 # Accept: [Accept: header value from RFC2616,
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html]
# Available: [whitespace-separated content types]
# Expected: [Accept-header like list, containing the available content
# types with their q-values]
Accept: */*
Available: text/plain
Expected: text/plain; q=1.0
Accept: */*
Available: text/plain, text/html
Expected: text/plain; q=1.0, text/html; q=1.0
# The order matters
Accept: */*
Available: text/html, text/plain
Expected: text/html; q=1.0, text/plain; q=1.0
Accept: text/*, */*; q=0.9
Available: text/plain, image/jpeg
Expected: text/plain; q=1.0, image/jpeg; q=0.9
Accept: text/*, */*; q=0.9
Available: image/jpeg, text/plain
Expected: text/plain; q=1.0, image/jpeg; q=0.9
# wildcard subtypes still reject differing main types
Accept: text/*
Available: image/jpeg, text/plain
Expected: text/plain; q=1.0
Accept: text/html
Available: text/html
Expected: text/html; q=1.0
Accept: text/html, text/*
Available: text/html
Expected: text/html; q=1.0
Accept: text/html, text/*
Available: text/plain, text/html
Expected: text/plain; q=1.0, text/html; q=1.0
Accept: text/html, text/*; q=0.9
Available: text/plain, text/html
Expected: text/html; q=1.0, text/plain; q=0.9
# If a more specific type has a higher q-value, then the higher value wins
Accept: text/*; q=0.9, text/html
Available: text/plain, text/html
Expected: text/html; q=1.0, text/plain; q=0.9
Accept: */*, text/*; q=0.9, text/html; q=0.1
Available: text/plain, text/html, image/monkeys
Expected: image/monkeys; q=1.0, text/plain; q=0.9, text/html; q=0.1
Accept: text/*, text/html; q=0
Available: text/html
Expected:
Accept: text/*, text/html; q=0
Available: text/html, text/plain
Expected: text/plain; q=1.0
Accept: text/html
Available: text/plain
Expected:
Accept: application/xrds+xml, text/html; q=0.9
Available: application/xrds+xml, text/html
Expected: application/xrds+xml; q=1.0, text/html; q=0.9
Accept: application/xrds+xml, */*; q=0.9
Available: application/xrds+xml, text/html
Expected: application/xrds+xml; q=1.0, text/html; q=0.9
Accept: application/xrds+xml, application/xhtml+xml; q=0.9, text/html; q=0.8, text/xml; q=0.7
Available: application/xrds+xml, text/html
Expected: application/xrds+xml; q=1.0, text/html; q=0.8
# See http://www.rfc-editor.org/rfc/rfc3023.txt, section A.13
Accept: application/xrds
Available: application/xrds+xml
Expected:
Accept: application/xrds+xml
Available: application/xrds
Expected:
Accept: application/xml
Available: application/xrds+xml
Expected:
Available: application/xrds+xml
Accept: application/xml
Expected:
#################################################
# The tests below this line are documentation of how this library
# works. If the implementation changes, it's acceptable to change the
# test to reflect that. These are specified so that we can make sure
# that the current implementation actually works the way that we
# expect it to given these inputs.
Accept: text/html;level=1
Available: text/html
Expected: text/html; q=1.0
Accept: text/html; level=1, text/html; level=9; q=0.1
Available: text/html
Expected: text/html; q=1.0
Accept: text/html; level=9; q=0.1, text/html; level=1
Available: text/html
Expected: text/html; q=1.0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/test/data/example-xrds.xml 0000644 0001750 0001750 00000000463 00000000000 022603 0 ustar 00rami rami 0000000 0000000
http://example.com/http://www.openidenabled.com/
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/test/data/openid-1.2-consumer-sqlitestore.db 0000644 0001750 0001750 00000016000 00000000000 025732 0 ustar 00rami rami 0000000 0000000 SQLite format 3 @
ü >ÇÐý— %%gtableoid_settingsoid_settingsCREATE TABLE oid_settings
(
setting VARCHAR(128) UNIQUE PRIMARY KEY,
value BLOB(20)
)7K% indexsqlite_autoindex_oid_settings_1oid_settings‚*--„tableoid_associationsoid_associationsCREATE TABLE oid_associations
(
server_url VARCHAR(2047),
handle VARCHAR(255),
secret BLOB(128),
issued INTEGER,
lifetime INTEGER,
assoc_type VARCHAR(64),
PRIMARY KEY (server_url, handle)
)?S- indexsqlite_autoindex_oid_associations_1oid_associations!!Wtableoid_noncesoid_noncesCREATE TABLE oid_nonces
(
nonce CHAR(8) UNIQUE PRIMARY KEY,
expires INTEGER
)3G! indexsqlite_autoindex_oid_nonces_1oid_nonces
ï ï uVgtgowPEæE´
ó ó uVgtgowP
¦ |¦ jMK4http://openid.claimid.com/server{HMAC-SHA1}{45e645b5}{B4avkw==}Äñιeð@JµøÑ¶Ÿn:éçEæEµØ€HMAC-SHA1hIK4http://www.myopenid.com/server{HMAC-SHA1}{45e645a2}{ipZo+w==}´FcÈð×Ð?ÎYé6„v“²EæE¢u HMAC-SHA1ea4http://www.livejournal.com/openid/server.bml1172718996:5boOvscaSb3EccDEchnG:99414af06a~Ç ™«h,QîüTËF#FBüEæE•qHMAC-SHA1
¤a DMKhttp://openid.claimid.com/server{HMAC-SHA1}{45e645b5}{B4avkw==}BIKhttp://www.myopenid.com/server{HMAC-SHA1}{45e645a2}{ipZo+w==}[eahttp://www.livejournal.com/openid/server.bml1172718996:5boOvscaSb3EccDEchnG:99414af06a
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/test/data/test1-discover.txt 0000644 0001750 0001750 00000004132 00000000000 023062 0 ustar 00rami rami 0000000 0000000 equiv
Status: 200 OK
Content-Type: text/html
Joe Schmoe's Homepage
Joe Schmoe's Homepage
Blah blah blah blah blah blah blah
header
Status: 200 OK
Content-Type: text/html
YADIS_HEADER: URL_BASE/xrds
Joe Schmoe's Homepage
Joe Schmoe's Homepage
Blah blah blah blah blah blah blah
xrds
Status: 200 OK
Content-Type: application/xrds+xml
xrds_ctparam
Status: 200 OK
Content-Type: application/xrds+xml; charset=UTF8
xrds_ctcase
Status: 200 OK
Content-Type: appliCATION/XRDS+xml
xrds_html
Status: 200 OK
Content-Type: text/html
redir_equiv
Status: 302 Found
Content-Type: text/plain
Location: URL_BASE/equiv
You are presently being redirected.
redir_header
Status: 302 Found
Content-Type: text/plain
Location: URL_BASE/header
You are presently being redirected.
redir_xrds
Status: 302 Found
Content-Type: application/xrds+xml
Location: URL_BASE/xrds
redir_xrds_html
Status: 302 Found
Content-Type: text/plain
Location: URL_BASE/xrds_html
You are presently being redirected.
redir_redir_equiv
Status: 302 Found
Content-Type: text/plain
Location: URL_BASE/redir_equiv
You are presently being redirected.
lowercase_header
Status: 200 OK
Content-Type: text/html
x-xrds-location: URL_BASE/xrds
Joe Schmoe's Homepage
Joe Schmoe's Homepage
Blah blah blah blah blah blah blah
404_server_response
Status: 404 Not Found
EEk!
500_server_response
Status: 500 Server error
EEk!
201_server_response
Status: 201 Created
EEk!
404_with_header
Status: 404 Not Found
YADIS_HEADER: URL_BASE/xrds
EEk!
404_with_meta
Status: 404 Not Found
Content-Type: text/html
Joe Schmoe's Homepage
Joe Schmoe's Homepage
Blah blah blah blah blah blah blah
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 011453 x ustar 00 0000000 0000000 22 mtime=1586707836.0
python3-openid-3.2.0/openid/test/data/test1-parsehtml.txt 0000644 0001750 0001750 00000006672 00000000000 023256 0 ustar 00rami rami 0000000 0000000 found
found
found
found
found
found
found
found
EOF
Name: Link inside comment inside head inside html
Name: Link inside of head after short head
Name: Plain vanilla
Link:
Name: Ignore tags in the namespace
Link*:
Name: Short link tag
Link:
Name: Spaces in the HTML tag
Link:
Name: Spaces in the head tag
Link:
Name: Spaces in the link tag
Link:
Name: No whitespace
Link:
Name: Closed head tag
Link:
Name: One good, one bad (after close head)
Link:
Name: One good, one bad (after open body)
Link:
Name: ill formed (missing close head)
Link:
Name: Ill formed (no close head, link after )
Link:
Name: Ignore random tags inside of html
Link:
Name: case-folding
Link*:
Name: unexpected tags
Link:
Name: un-closed script tags
Link*: