summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDuane King <duaneking@users.noreply.github.com>2019-01-24 12:01:14 -0800
committerGitHub <noreply@github.com>2019-01-24 12:01:14 -0800
commit0b5509e1bc809b4861124829d24c3cb2c8162720 (patch)
tree749c07ef87445a91cd1ee909ddf13aa80789d387
parentfb7ec207b17e0cacf52f9f3c2643c4b9036d827c (diff)
parent575638ce7ddb8727e08980235ccd82152af85703 (diff)
downloadoauthlib-0b5509e1bc809b4861124829d24c3cb2c8162720.tar.gz
Merge pull request #1 from oauthlib/master
Merge Main into personal dev branch.
-rw-r--r--.coveragerc20
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md24
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md14
-rw-r--r--.gitignore5
-rw-r--r--.travis.yml69
-rw-r--r--AUTHORS3
-rw-r--r--CHANGELOG.rst83
-rw-r--r--CODE_OF_CONDUCT.md28
-rw-r--r--LICENSE4
-rw-r--r--Makefile95
-rw-r--r--README.rst66
-rw-r--r--bandit.json48
-rw-r--r--docs/Makefile2
-rw-r--r--docs/conf.py22
-rw-r--r--docs/contributing.rst108
-rw-r--r--docs/faq.rst31
-rw-r--r--docs/feature_matrix.rst53
-rw-r--r--docs/index.rst8
-rw-r--r--docs/installation.rst4
-rw-r--r--docs/oauth1/client.rst14
-rw-r--r--docs/oauth1/preconfigured_servers.rst2
-rw-r--r--docs/oauth1/security.rst12
-rw-r--r--docs/oauth1/server.rst6
-rw-r--r--docs/oauth2/clients/client.rst2
-rw-r--r--docs/oauth2/endpoints/endpoints.rst12
-rw-r--r--docs/oauth2/endpoints/introspect.rst26
-rw-r--r--docs/oauth2/endpoints/metadata.rst72
-rw-r--r--docs/oauth2/grants/jwt.rst17
-rw-r--r--docs/oauth2/oauth2provider-legend.dot32
-rw-r--r--docs/oauth2/oauth2provider-server.dot268
-rw-r--r--docs/oauth2/oidc/id_tokens.rst35
-rw-r--r--docs/oauth2/oidc/validator.rst29
-rw-r--r--docs/oauth2/preconfigured_servers.rst6
-rw-r--r--docs/oauth2/server.rst49
-rw-r--r--docs/oauth2/tokens/bearer.rst116
-rw-r--r--docs/oauth2/tokens/mac.rst2
-rw-r--r--docs/oauth2/tokens/saml.rst2
-rw-r--r--docs/oauth2/tokens/tokens.rst7
-rw-r--r--docs/release_process.rst10
-rw-r--r--examples/skeleton_oauth2_web_application_server.py2
-rw-r--r--oauthlib/__init__.py16
-rw-r--r--oauthlib/common.py41
-rw-r--r--oauthlib/oauth1/__init__.py2
-rw-r--r--oauthlib/oauth1/rfc5849/__init__.py24
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/access_token.py8
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/authorization.py3
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/base.py6
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/request_token.py10
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/resource.py2
-rw-r--r--oauthlib/oauth1/rfc5849/parameters.py22
-rw-r--r--oauthlib/oauth1/rfc5849/request_validator.py69
-rw-r--r--oauthlib/oauth1/rfc5849/signature.py135
-rw-r--r--oauthlib/oauth1/rfc5849/utils.py4
-rw-r--r--oauthlib/oauth2/__init__.py4
-rw-r--r--oauthlib/oauth2/rfc6749/clients/backend_application.py27
-rw-r--r--oauthlib/oauth2/rfc6749/clients/base.py45
-rw-r--r--oauthlib/oauth2/rfc6749/clients/legacy_application.py23
-rw-r--r--oauthlib/oauth2/rfc6749/clients/mobile_application.py20
-rw-r--r--oauthlib/oauth2/rfc6749/clients/service_application.py39
-rw-r--r--oauthlib/oauth2/rfc6749/clients/web_application.py59
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/__init__.py2
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/authorization.py2
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/base.py25
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/introspect.py122
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/metadata.py242
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/pre_configured.py64
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/resource.py2
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/revocation.py42
-rw-r--r--oauthlib/oauth2/rfc6749/endpoints/token.py2
-rw-r--r--oauthlib/oauth2/rfc6749/errors.py239
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/__init__.py8
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/authorization_code.py213
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/base.py126
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/client_credentials.py22
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/implicit.py80
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/refresh_token.py25
-rw-r--r--oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py25
-rw-r--r--oauthlib/oauth2/rfc6749/parameters.py120
-rw-r--r--oauthlib/oauth2/rfc6749/request_validator.py397
-rw-r--r--oauthlib/oauth2/rfc6749/tokens.py94
-rw-r--r--oauthlib/openid/__init__.py9
-rw-r--r--oauthlib/openid/connect/__init__.py0
-rw-r--r--oauthlib/openid/connect/core/__init__.py0
-rw-r--r--oauthlib/openid/connect/core/endpoints/__init__.py11
-rw-r--r--oauthlib/openid/connect/core/endpoints/pre_configured.py107
-rw-r--r--oauthlib/openid/connect/core/exceptions.py152
-rw-r--r--oauthlib/openid/connect/core/grant_types/__init__.py17
-rw-r--r--oauthlib/openid/connect/core/grant_types/authorization_code.py24
-rw-r--r--oauthlib/openid/connect/core/grant_types/base.py (renamed from oauthlib/oauth2/rfc6749/grant_types/openid_connect.py)182
-rw-r--r--oauthlib/openid/connect/core/grant_types/dispatchers.py91
-rw-r--r--oauthlib/openid/connect/core/grant_types/exceptions.py32
-rw-r--r--oauthlib/openid/connect/core/grant_types/hybrid.py36
-rw-r--r--oauthlib/openid/connect/core/grant_types/implicit.py28
-rw-r--r--oauthlib/openid/connect/core/request_validator.py195
-rw-r--r--oauthlib/openid/connect/core/tokens.py54
-rw-r--r--oauthlib/signals.py2
-rw-r--r--requirements-test.txt6
-rw-r--r--requirements.txt6
-rw-r--r--setup.cfg3
-rwxr-xr-xsetup.py17
-rw-r--r--tests/oauth1/rfc5849/endpoints/test_authorization.py2
-rw-r--r--tests/oauth1/rfc5849/endpoints/test_base.py2
-rw-r--r--tests/oauth1/rfc5849/test_client.py52
-rw-r--r--tests/oauth2/rfc6749/clients/test_backend_application.py1
-rw-r--r--tests/oauth2/rfc6749/clients/test_base.py72
-rw-r--r--tests/oauth2/rfc6749/clients/test_legacy_application.py62
-rw-r--r--tests/oauth2/rfc6749/clients/test_mobile_application.py14
-rw-r--r--tests/oauth2/rfc6749/clients/test_service_application.py70
-rw-r--r--tests/oauth2/rfc6749/clients/test_web_application.py99
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_base_endpoint.py4
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_client_authentication.py60
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py22
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_error_responses.py27
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py141
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_metadata.py126
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py1
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py23
-rw-r--r--tests/oauth2/rfc6749/endpoints/test_scope_handling.py1
-rw-r--r--tests/oauth2/rfc6749/grant_types/test_authorization_code.py130
-rw-r--r--tests/oauth2/rfc6749/grant_types/test_openid_connect.py403
-rw-r--r--tests/oauth2/rfc6749/grant_types/test_refresh_token.py4
-rw-r--r--tests/oauth2/rfc6749/test_parameters.py17
-rw-r--r--tests/oauth2/rfc6749/test_server.py102
-rw-r--r--tests/oauth2/rfc6749/test_tokens.py20
-rw-r--r--tests/openid/__init__.py0
-rw-r--r--tests/openid/connect/__init__.py0
-rw-r--r--tests/openid/connect/core/__init__.py0
-rw-r--r--tests/openid/connect/core/endpoints/__init__.py0
-rw-r--r--tests/openid/connect/core/endpoints/test_claims_handling.py (renamed from tests/oauth2/rfc6749/endpoints/test_claims_handling.py)16
-rw-r--r--tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py (renamed from tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py)8
-rw-r--r--tests/openid/connect/core/grant_types/__init__.py0
-rw-r--r--tests/openid/connect/core/grant_types/test_authorization_code.py150
-rw-r--r--tests/openid/connect/core/grant_types/test_dispatchers.py125
-rw-r--r--tests/openid/connect/core/grant_types/test_hybrid.py14
-rw-r--r--tests/openid/connect/core/grant_types/test_implicit.py140
-rw-r--r--tests/openid/connect/core/test_request_validator.py52
-rw-r--r--tests/openid/connect/core/test_server.py180
-rw-r--r--tests/openid/connect/core/test_tokens.py133
-rw-r--r--tests/test_common.py20
-rw-r--r--tests/unittest/__init__.py21
-rw-r--r--tox.ini33
141 files changed, 5545 insertions, 1593 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..70666c7
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,20 @@
+[run]
+branch = 1
+cover_pylib = 0
+include=*oauthlib/*
+omit = oauthlib.tests.*
+
+[report]
+omit =
+ */python?.?/*
+ */site-packages/*
+ */pypy/*
+exclude_lines =
+ pragma: no cover
+ def __repr__
+ if __debug__:
+ raise AssertionError
+ raise NotImplementedError
+ if 0:
+ if __name__ == .__main__.:
+ noqa
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..4c5a84b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,24 @@
+---
+name: Bug report
+about: Create a report to help us improve
+---
+**Describe the bug**
+
+A clear and concise description of what the problem is.
+
+**How to reproduce**
+
+Steps to reproduce the behavior.
+
+**Expected behavior**
+
+A description of what you expected to happen.
+
+**Additional context**
+
+Please provide any further context here.
+
+- Are you using OAuth1, OAuth2 or OIDC?
+- Are you writing client or server side code?
+- If client, what provider are you connecting to?
+- Are you using a downstream library, such as `requests-oauthlib`, `django-oauth-toolkit`, ...?
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..a415f6c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,14 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+---
+**Describe the feature**
+
+A clear and concise description of what you would like to see.
+
+**Additional context**
+
+Please provide any further context here.
+
+- Does the feature apply to OAuth1, OAuth2 and/or OIDC?
+- Does the feature apply to client or server side code?
diff --git a/.gitignore b/.gitignore
index 4515c8f..a3f5614 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
*.pyc
+.idea
*.sublime-project
*.sublime-workspace
*.swp
@@ -22,9 +23,11 @@ develop-eggs
pip-log.txt
# Unit test / coverage reports
+.cache
.coverage
.tox
coverage
+htmlcov*
#Translations
*.mo
@@ -32,6 +35,8 @@ coverage
# Local file cruft/auto-backups
.DS_Store
*~
+**/#*#
+**/.#*
# Sphinx
docs/_build
diff --git a/.travis.yml b/.travis.yml
index 4dee48b..c7978d7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,33 +1,54 @@
language: python
+python: 3.7
+dist: xenial
sudo: false
cache: pip
-
matrix:
include:
- - python: 2.7
- env: TOXENV=py27
- - python: 3.4
- env: TOXENV=py34
- - python: 3.5
- env: TOXENV=py35
- - python: 3.6
- env: TOXENV=py36
- - python: pypy-5.3
- env: TOXENV=pypy
-
+ - python: 2.7
+ env: TOXENV=py27
+ - python: 3.4
+ env: TOXENV=py34
+ - python: 3.5
+ env: TOXENV=py35
+ - python: 3.6
+ env: TOXENV=py36
+ - python: 3.7
+ env: TOXENV=py37
+ - python: 3.7
+ env: TOXENV=bandit
+ - python: pypy3.5
+ env: TOXENV=pypy3
install:
- - pip install -U setuptools
- - pip install tox coveralls
+- pip install -U setuptools
+- pip install tox coveralls
script: tox
-
-after_success: coveralls
+after_success: COVERALLS_PARALLEL=true coveralls
notifications:
- irc: irc.freenode.org#oauthlib
+ webhooks:
+ urls:
+ - https://coveralls.io/webhook
+ - https://webhooks.gitter.im/e/6008c872bf0ecee344f4
+ on_success: change
+ on_failure: always
+ on_start: never
deploy:
- provider: pypi
- user: ib.lundgren
- password:
- secure: PGZF9pRiTGCSwQjk1ddTKF3x4rQ0iAiPbg2uSixyO68uMXRgJjwHhSrNM0OEqtK5YWU5FE5L0DwR1nkrpEJKO4a5q2EOgos+gVoKpJfinoUNOOkjc1VHpqKM0uRf/OKrw1alvWUwqvW8B+DOb9TY5c5VZxQuRL+iwdrtwzFlKls=
- on:
- tags: true
- repo: idan/oauthlib
+ - provider: pypi
+ user: JonathanHuot
+ password:
+ secure: "OozNM16flVLvqDoNzmoTENchhS1w0/dEJZvXBQK2KWmh8fyGj2UZus1vkl6bA5V3Yu9MZLYFpDcltl/qraY3Up6iXQpwKz4q+ICygAudYM2kJ5l8ZEe+wy2FikWbD6LkXf5uKIJJnPNSC8AI86ZyxM/XZxbYjj/+jXyJ1YFZwwQ="
+ distributions: sdist bdist_wheel
+ on:
+ tags: true
+ all_branches: true
+ condition: $TOXENV = py36
+ repo: oauthlib/oauthlib
+ - provider: releases
+ api_key:
+ secure: LEzTaeQt4+Sp21t7usmwaEYLThKIGWDNNj04JADMLgfquTeyz5nDu9P8JNlT//G9RNN20oR8w7jZo97Y+JAylq6Hh/I+p/MEzZi8+NwIpObk3n3zJO4witZQQSTEw/6B7qf1/NQQxjQzlYTJjsGXxBps7srviWZmbH6Tz+epA3A=
+ skip_cleanup: true
+ on:
+ tags: true
+ all_branches: true
+ condition: $TOXENV = py36
+ repo: oauthlib/oauthlib
diff --git a/AUTHORS b/AUTHORS
index 811679e..f52ce9a 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -26,3 +26,6 @@ Juan Fabio García Solero
Omer Katz
Joel Stevenson
Brendan McCollam
+Jonathan Huot
+Pieter Ennes
+Olaf Conradi
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 8a20f92..2cc0dd3 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,89 @@
Changelog
=========
+3.0.0 (2019-01-01)
+------------------
+OAuth2.0 Provider - outstanding Features
+
+* OpenID Connect Core support
+* RFC7662 Introspect support
+* RFC8414 OAuth2.0 Authorization Server Metadata support (#605)
+* RFC7636 PKCE support (#617 #624)
+
+OAuth2.0 Provider - API/Breaking Changes
+
+* Add "request" to confirm_redirect_uri #504
+* confirm_redirect_uri/get_default_redirect_uri has a bit changed #445
+* invalid_client is now a FatalError #606
+* Changed errors status code from 401 to 400:
+ - invalid_grant: #264
+ - invalid_scope: #620
+ - access_denied/unauthorized_client/consent_required/login_required #623
+ - 401 must have WWW-Authenticate HTTP Header set. #623
+
+OAuth2.0 Provider - Bugfixes
+
+* empty scopes no longer raise exceptions for implicit and authorization_code #475 / #406
+
+OAuth2.0 Client - Bugfixes / Changes:
+
+* expires_in in Implicit flow is now an integer #569
+* expires is no longer overriding expires_in #506
+* parse_request_uri_response is now required #499
+* Unknown error=xxx raised by OAuth2 providers was not understood #431
+* OAuth2's `prepare_token_request` supports sending an empty string for `client_id` (#585)
+* OAuth2's `WebApplicationClient.prepare_request_body` was refactored to better
+ support sending or omitting the `client_id` via a new `include_client_id` kwarg.
+ By default this is included. The method will also emit a DeprecationWarning if
+ a `client_id` parameter is submitted; the already configured `self.client_id`
+ is the preferred option. (#585)
+
+OAuth1.0 Client:
+
+* Support for HMAC-SHA256 #498
+
+General fixes:
+
+* $ and ' are allowed to be unencoded in query strings #564
+* Request attributes are no longer overriden by HTTP Headers #409
+* Removed unnecessary code for handling python2.6
+* Add support of python3.7 #621
+* Several minors updates to setup.py and tox
+* Set pytest as the default unittest framework
+
+
+2.1.0 (2018-05-21)
+------------------
+
+* Fixed some copy and paste typos (#535)
+* Use secrets module in Python 3.6 and later (#533)
+* Add request argument to confirm_redirect_uri (#504)
+* Avoid populating spurious token credentials (#542)
+* Make populate attributes API public (#546)
+
+2.0.7 (2018-03-19)
+------------------
+
+* Moved oauthlib into new organization on GitHub.
+* Include license file in the generated wheel package. (#494)
+* When deploying a release to PyPI, include the wheel distribution. (#496)
+* Check access token in self.token dict. (#500)
+* Added bottle-oauthlib to docs. (#509)
+* Update repository location in Travis. (#514)
+* Updated docs for organization change. (#515)
+* Replace G+ with Gitter. (#517)
+* Update requirements. (#518)
+* Add shields for Python versions, license and RTD. (#520)
+* Fix ReadTheDocs build (#521).
+* Fixed "make" command to test upstream with local oauthlib. (#522)
+* Replace IRC notification with Gitter Hook. (#523)
+* Added Github Releases deploy provider. (#523)
+
+2.0.6 (2017-10-20)
+------------------
+
+* 2.0.5 contains breaking changes.
+
2.0.5 (2017-10-19)
------------------
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..3f242ff
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,28 @@
+# OAuthlib Code of Conduct
+
+Like the technical community as a whole, the OAuthlib team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people.
+
+Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance.
+
+This isn't an exhaustive list of things that you can't do. Rather, take it in the spirit in which it's intended - a guide to make it easier to enrich all of us and the technical communities in which we participate.
+
+This code of conduct applies to all spaces managed by the OAuthlib project. This includes Gitter, the mailing lists, the issue tracker, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them.
+
+If you believe someone is violating the code of conduct, we ask that you report it by contacting us.
+
+ Be friendly and patient.
+ Be welcoming. We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
+ Be considerate. Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
+ Be respectful. Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It's important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the OAuthlib community should be respectful when dealing with other members as well as with people outside the OAuthlib community.
+ Be careful in the words that you choose. We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
+ Violent threats or language directed against another person.
+ Discriminatory jokes and language.
+ Posting sexually explicit or violent material.
+ Posting (or threatening to post) other people's personally identifying information ("doxing").
+ Personal insults, especially those using racist or sexist terms.
+ Unwelcome sexual attention.
+ Advocating for, or encouraging, any of the above behavior.
+ Repeated harassment of others. In general, if someone asks you to stop, then stop.
+ When we disagree, try to understand why. Disagreements, both social and technical, happen all the time and OAuthlib is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we're different. The strength of OAuthlib comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn't mean that they're wrong. Don't forget that it is human to err and blaming each other doesn't get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
+
+For reading the original text, please visit the [Django Code of Conduct](https://www.djangoproject.com/conduct/).
diff --git a/LICENSE b/LICENSE
index c10d256..d5a9e9a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011 Idan Gazit and contributors
+Copyright (c) 2019 The OAuthlib Community
All rights reserved.
Redistribution and use in source and binary forms, with or without
@@ -24,4 +24,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
index 259fe9c..64fdc8e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,47 +1,78 @@
-PYS = py27,py34,pypy
+# Downstream tests (Don't be evil)
+#
+# Try and not break the libraries below by running their tests too.
+#
+# Unfortunately there is no neat way to run downstream tests AFAIK
+# Until we have a proper downstream testing system we will
+# stick to this Makefile.
+#---------------------------
+# HOW TO ADD NEW DOWNSTREAM LIBRARIES
+#
+# Please specify your library as well as primary contacts.
+# Since these contacts will be addressed with Github mentions they
+# need to be Github users (for now)(sorry Bitbucket).
+#
+clean: clean-eggs clean-build
+ @find . -iname '*.pyc' -delete
+ @find . -iname '*.pyo' -delete
+ @find . -iname '*~' -delete
+ @find . -iname '*.swp' -delete
+ @find . -iname '__pycache__' -delete
+ rm -rf .tox
+ rm -rf bottle-oauthlib
+ rm -rf dance
+ rm -rf django-oauth-toolkit
+ rm -rf flask-oauthlib
+ rm -rf requests-oauthlib
+
+clean-eggs:
+ @find . -name '*.egg' -print0|xargs -0 rm -rf --
+ @rm -rf .eggs/
+
+clean-build:
+ @rm -fr build/
+ @rm -fr dist/
+ @rm -fr *.egg-info
test:
- # Test OAuthLib
- tox -e "$(PYS)"
- #
- # Downstream tests (Don't be evil)
- #
- # Try and not break the libraries below by running their tests too.
- #
- # Unfortunately there is no neat way to run downstream tests AFAIK
- # Until we have a proper downstream testing system we will
- # stick to this Makefile.
+ tox
+
+bottle:
#---------------------------
- # HOW TO ADD NEW DOWNSTREAM LIBRARIES
- #
- # Please specify your library as well as primary contacts.
- # Since these contacts will be addressed with Github mentions they
- # need to be Github users (for now)(sorry Bitbucket).
- #
+ # Library thomsonreuters/bottle-oauthlib
+ # Contacts: Jonathan.Huot
+ cd bottle-oauthlib 2>/dev/null || git clone https://github.com/thomsonreuters/bottle-oauthlib.git
+ cd bottle-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox
+
+flask:
#---------------------------
# Library: lepture/flask-oauthLib
# Contacts: lepture,widnyana
- git clone https://github.com/lepture/flask-oauthlib.git
- cd flask-oauthlib && cp ../tox.ini . && sed -i 's/py32,py33,py34,//' tox.ini && sed -i '/mock/a \ Flask-SQLAlchemy' tox.ini && tox -e "$(PYS)"
- rm -rf flask-oauthlib
+ cd flask-oauthlib 2>/dev/null || git clone https://github.com/lepture/flask-oauthlib.git
+ cd flask-oauthlib && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox
+
+django:
#---------------------------
# Library: evonove/django-oauth-toolkit
# Contacts: evonove,masci
# (note: has tox.ini already)
- git clone https://github.com/evonove/django-oauth-toolkit.git
- cd django-oauth-toolkit && tox -e "$(PYS)"
- rm -rf django-oauth-toolkit
+ cd django-oauth-toolkit 2>/dev/null || git clone https://github.com/evonove/django-oauth-toolkit.git
+ cd django-oauth-toolkit && sed -i.old 's,deps =,deps= --editable=file://{toxinidir}/../,' tox.ini && tox -e py27,py35,py36
+
+requests:
#---------------------------
# Library requests/requests-oauthlib
# Contacts: ib-lundgren,lukasa
- git clone https://github.com/requests/requests-oauthlib.git
- cd requests-oauthlib && cp ../tox.ini . && sed -i '/mock/a \ requests' tox.ini && tox -e "$(PYS)"
- rm -rf requests-oauthlib
- #---------------------------
- #
+ cd requests-oauthlib 2>/dev/null || git clone https://github.com/requests/requests-oauthlib.git
+ cd requests-oauthlib && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../[signedtoken],' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox
-pycco:
- find oauthlib -name "*.py" -exec pycco -p -s reST {} \;
+dance:
+ #---------------------------
+ # Library singingwolfboy/flask-dance
+ # Contacts: singingwolfboy
+ cd flask-dance 2>/dev/null || git clone https://github.com/singingwolfboy/flask-dance.git
+ cd flask-dance && sed -i.old 's,deps=,deps = --editable=file://{toxinidir}/../,' tox.ini && sed -i.old '/oauthlib/d' requirements.txt && tox
-pycco-clean:
- rm -rf docs/oauthlib docs/pycco.css
+.DEFAULT_GOAL := all
+.PHONY: clean test bottle dance django flask requests
+all: clean test bottle dance django flask requests
diff --git a/README.rst b/README.rst
index eb85ffa..7c41a80 100644
--- a/README.rst
+++ b/README.rst
@@ -1,14 +1,30 @@
-OAuthLib
-========
+OAuthLib - Python Framework for OAuth1 & OAuth2
+===============================================
*A generic, spec-compliant, thorough implementation of the OAuth request-signing
-logic for python*
-
-.. image:: https://travis-ci.org/idan/oauthlib.svg?branch=master
- :target: https://travis-ci.org/idan/oauthlib
-.. image:: https://coveralls.io/repos/idan/oauthlib/badge.svg?branch=master
- :target: https://coveralls.io/r/idan/oauthlib
-
+logic for Python 2.7 and 3.4+.*
+
+.. image:: https://travis-ci.org/oauthlib/oauthlib.svg?branch=master
+ :target: https://travis-ci.org/oauthlib/oauthlib
+ :alt: Travis
+.. image:: https://coveralls.io/repos/oauthlib/oauthlib/badge.svg?branch=master
+ :target: https://coveralls.io/r/oauthlib/oauthlib
+ :alt: Coveralls
+.. image:: https://img.shields.io/pypi/pyversions/oauthlib.svg
+ :target: https://pypi.org/project/oauthlib/
+ :alt: Download from PyPI
+.. image:: https://img.shields.io/pypi/l/oauthlib.svg
+ :target: https://pypi.org/project/oauthlib/
+ :alt: License
+.. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib.svg?type=shield
+ :target: https://app.fossa.io/projects/git%2Bgithub.com%2Foauthlib%2Foauthlib?ref=badge_shield
+ :alt: FOSSA Status
+.. image:: https://img.shields.io/readthedocs/oauthlib.svg
+ :target: https://oauthlib.readthedocs.io/en/latest/index.html
+ :alt: Read the Docs
+.. image:: https://badges.gitter.im/oauthlib/oauthlib.svg
+ :target: https://gitter.im/oauthlib/Lobby
+ :alt: Chat on Gitter
OAuth often seems complicated and difficult-to-implement. There are several
prominent libraries for handling OAuth requests, but they all suffer from one or
@@ -18,10 +34,10 @@ both of the following:
2. They predate the `OAuth 2.0 spec`_, AKA RFC 6749.
3. They assume the usage of a specific HTTP request library.
-.. _`OAuth 1.0 spec`: http://tools.ietf.org/html/rfc5849
-.. _`OAuth 2.0 spec`: http://tools.ietf.org/html/rfc6749
+.. _`OAuth 1.0 spec`: https://tools.ietf.org/html/rfc5849
+.. _`OAuth 2.0 spec`: https://tools.ietf.org/html/rfc6749
-OAuthLib is a generic utility which implements the logic of OAuth without
+OAuthLib is a framework which implements the logic of OAuth1 or OAuth2 without
assuming a specific HTTP request object or web framework. Use it to graft OAuth
client support onto your favorite HTTP library, or provide support onto your
favourite web framework. If you're a maintainer of such a library, write a thin
@@ -33,10 +49,10 @@ Documentation
Full documentation is available on `Read the Docs`_. All contributions are very
welcome! The documentation is still quite sparse, please open an issue for what
-you'd like to know, or discuss it in our `G+ community`_, or even better, send a
+you'd like to know, or discuss it in our `Gitter community`_, or even better, send a
pull request!
-.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
+.. _`Gitter community`: https://gitter.im/oauthlib/Lobby
.. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html
Interested in making OAuth requests?
@@ -45,7 +61,7 @@ Interested in making OAuth requests?
Then you might be more interested in using `requests`_ which has OAuthLib
powered OAuth support provided by the `requests-oauthlib`_ library.
-.. _`requests`: https://github.com/kennethreitz/requests
+.. _`requests`: https://github.com/requests/requests
.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib
Which web frameworks are supported?
@@ -56,6 +72,7 @@ The following packages provide OAuth support using OAuthLib.
- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
- For Pyramid there is `pyramid-oauthlib`_.
+- For Bottle there is `bottle-oauthlib`_.
If you have written an OAuthLib package that supports your favorite framework,
please open a Pull Request, updating the documentation.
@@ -65,6 +82,7 @@ please open a Pull Request, updating the documentation.
.. _`Django REST framework`: http://django-rest-framework.org
.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
+.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
Using OAuthLib? Please get in touch!
------------------------------------
@@ -72,7 +90,7 @@ Patching OAuth support onto an http request framework? Creating an OAuth
provider extension for a web framework? Simply using OAuthLib to Get Things Done
or to learn?
-No matter which we'd love to hear from you in our `G+ community`_ or if you have
+No matter which we'd love to hear from you in our `Gitter community`_ or if you have
anything in particular you would like to have, change or comment on don't
hesitate for a second to send a pull request or open an issue. We might be quite
busy and therefore slow to reply but we love feedback!
@@ -81,7 +99,7 @@ Chances are you have run into something annoying that you wish there was
documentation for, if you wish to gain eternal fame and glory, and a drink if we
have the pleasure to run into eachother, please send a docs pull request =)
-.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
+.. _`Gitter community`: https://gitter.im/oauthlib/Lobby
License
-------
@@ -89,10 +107,22 @@ License
OAuthLib is yours to use and abuse according to the terms of the BSD license.
Check the LICENSE file for full details.
+Credits
+-------
+
+OAuthLib has been started and maintained several years by Idan Gazit and other
+amazing `AUTHORS`_. Thanks to their wonderful work, the open-source `community`_
+creation has been possible and the project can stay active and reactive to users
+requests.
+
+
+.. _`AUTHORS`: https://github.com/oauthlib/oauthlib/blob/master/AUTHORS
+.. _`community`: https://github.com/oauthlib/
+
Changelog
---------
-*OAuthLib is in active development, with the core of both OAuth 1 and 2
+*OAuthLib is in active development, with the core of both OAuth1 and OAuth2
completed, for providers as well as clients.* See `supported features`_ for
details.
diff --git a/bandit.json b/bandit.json
new file mode 100644
index 0000000..02e15a8
--- /dev/null
+++ b/bandit.json
@@ -0,0 +1,48 @@
+{
+ "errors": [],
+ "generated_at": "2018-12-13T10:39:37Z",
+ "results": [
+ {
+ "code": "182 if request.body is not None and content_type_eligible:\n183 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))\n184 \n",
+ "filename": "oauthlib/oauth1/rfc5849/__init__.py",
+ "issue_confidence": "HIGH",
+ "issue_severity": "MEDIUM",
+ "issue_text": "Use of insecure MD2, MD4, MD5, or SHA1 hash function.",
+ "line_number": 183,
+ "line_range": [
+ 183
+ ],
+ "more_info": "https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b303-md5",
+ "test_id": "B303",
+ "test_name": "blacklist"
+ },
+ {
+ "code": "45 def __init__(self, endpoints, claims={}, raise_errors=True):\n46 assert isinstance(claims, dict)\n47 for endpoint in endpoints:\n",
+ "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py",
+ "issue_confidence": "HIGH",
+ "issue_severity": "LOW",
+ "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.",
+ "line_number": 46,
+ "line_range": [
+ 46
+ ],
+ "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html",
+ "test_id": "B101",
+ "test_name": "assert_used"
+ },
+ {
+ "code": "47 for endpoint in endpoints:\n48 assert isinstance(endpoint, BaseEndpoint)\n49 \n",
+ "filename": "oauthlib/oauth2/rfc6749/endpoints/metadata.py",
+ "issue_confidence": "HIGH",
+ "issue_severity": "LOW",
+ "issue_text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.",
+ "line_number": 48,
+ "line_range": [
+ 48
+ ],
+ "more_info": "https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html",
+ "test_id": "B101",
+ "test_name": "assert_used"
+ }
+ ]
+}
diff --git a/docs/Makefile b/docs/Makefile
index 9ec7a6d..d134c96 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -2,7 +2,7 @@
#
# You can set these variables from the command line.
-SPHINXOPTS =
+SPHINXOPTS = -v
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
diff --git a/docs/conf.py b/docs/conf.py
index fb14d05..f1a489a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -14,8 +14,6 @@
import os
import sys
-from oauthlib import __version__ as v
-
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -23,11 +21,16 @@ sys.path.insert(0, os.path.abspath('..'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+needs_sphinx = '1.1'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.viewcode']
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.viewcode',
+ 'sphinx.ext.graphviz'
+]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -43,7 +46,7 @@ master_doc = 'index'
# General information about the project.
project = u'OAuthLib'
-copyright = u'2012, Idan Gazit and the Python Community'
+copyright = u'2019, The OAuthlib Community'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -51,6 +54,7 @@ copyright = u'2012, Idan Gazit and the Python Community'
#
# The short X.Y version.
+from oauthlib import __version__ as v
version = v[:3]
# The full version, including alpha/beta/rc tags.
release = v
@@ -187,7 +191,7 @@ latex_elements = {
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'OAuthLib.tex', u'OAuthLib Documentation',
- u'Idan Gazit and the Python Community', 'manual'),
+ u'The OAuhthlib Community', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -217,7 +221,7 @@ latex_documents = [
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'oauthlib', u'OAuthLib Documentation',
- [u'Idan Gazit and the Python Community'], 1)
+ [u'The OAuthlib Community'], 1)
]
# If true, show URL addresses after external links.
@@ -231,7 +235,7 @@ man_pages = [
# dir menu entry, description, category)
texinfo_documents = [
('index', 'OAuthLib', u'OAuthLib Documentation',
- u'Idan Gazit and the Python Community', 'OAuthLib', 'One line description of project.',
+ u'The OAuthlib Community', 'OAuthLib', 'One line description of project.',
'Miscellaneous'),
]
@@ -243,3 +247,5 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
+
+linkcheck_ignore = ["https://github.com/oauthlib/oauthlib/issues/new"]
diff --git a/docs/contributing.rst b/docs/contributing.rst
index f3de44d..e101f70 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -30,12 +30,37 @@ personal label matching your GitHub ID will be assigned to that issue.
Feel free to propose issues that aren't described!
+oauthlib community rules
+========================
+
+oauthlib is a community of developers which adheres to a very simple set of
+rules.
+
+Code of Conduct
+---------------
+This project adheres to a `Code of Conduct`_ based on Django. As a community
+member you have to read and agree with it.
+
+For more information please contact us and/or visit the original
+`Django Code of Conduct`_ homepage.
+
+.. _`Code of Conduct`: https://github.com/oauthlib/oauthlib/blob/master/CODE_OF_CONDUCT.md
+.. _`Django Code of Conduct`: https://www.djangoproject.com/conduct/
+
+Code of Merit
+-------------
+Please read the community's `Code of Merit`_. Every contributor will know the
+real purpose of their contributions to this project.
+
+.. _`Code of Merit`: http://code-of-merit.org/
+
+
Setting up topic branches and generating pull requests
======================================================
While it's handy to provide useful code snippets in an issue, it is better for
you as a developer to submit pull requests. By submitting pull request your
-contribution to OpenComparison will be recorded by Github.
+contribution to OAuthlib will be recorded by Github.
In git it is best to isolate each topic or feature into a "topic branch". While
individual commits allow you control over how small individual changes are made
@@ -91,7 +116,7 @@ request only to have it rejected because it has diverged too far from master.
To pull in upstream changes::
- git remote add upstream https://github.com/idan/oauthlib.git
+ git remote add upstream https://github.com/oauthlib/oauthlib.git
git fetch upstream
Check the log to be sure that you actually want the changes, before merging::
@@ -102,7 +127,7 @@ Then merge the changes that you fetched::
git merge upstream/master
-For more info, see http://help.github.com/fork-a-repo/
+For more info, see https://help.github.com/fork-a-repo/
How to get your pull request accepted
=====================================
@@ -119,7 +144,7 @@ the project root via:
.. sourcecode:: bash
- $ python -m unittest discover
+ $ py.test
The first thing the core committers will do is run this command. Any pull
request that fails this test suite will be **rejected**.
@@ -127,7 +152,7 @@ request that fails this test suite will be **rejected**.
Testing multiple versions of Python
-----------------------------------
-OAuthLib supports Python 2.6, 2.7, 3.2, 3.3 and experimentally PyPy. Testing
+OAuthLib supports Python 2.7, 3.4, 3.5, 3.6 and PyPy. Testing
all versions conveniently can be done using `Tox`_.
.. sourcecode:: bash
@@ -148,7 +173,18 @@ version. For Ubuntu you can easily install all after adding one ppa.
$ sudo apt-get install pypy pypy-dev
.. _`Tox`: https://tox.readthedocs.io/en/latest/install.html
-.. _`virtualenv`: http://www.virtualenv.org/en/latest/#installation
+.. _`virtualenv`: https://virtualenv.pypa.io/en/latest/installation/
+
+Test upstream applications
+-----------------------------------
+
+Remember, OAuthLib is used by several 3rd party projects. If you think you
+submit a breaking change, confirm that other projects builds are not affected.
+
+.. sourcecode:: bash
+
+ $ make
+
If you add code you need to add tests!
--------------------------------------
@@ -202,18 +238,70 @@ Furthermore, the pixel shortage is over. We want to see:
* `grid` instead of `g`
* `my_function_that_does_things` instead of `mftdt`
+Be sure to write documentation!
+-------------------------------
+
+Documentation isn't just good, it's great - and necessary with large packages
+like OAuthlib. Please make sure the next person who reads your function/method
+can quickly understand what it does and how. Also, please ensure the parameters
+passed to each function are properly documented as well.
+
+The project has these goals/requests for docstrings that are designed to make
+the autogenerated documentation read more cleanly:
+
+#. Every parameter in the function should be listed in the docstring, and
+ should appear in the same order as they appear in the function itself.
+#. If you are unsure of the best wording for a parameter description, leave it
+ blank, but still include the `:param foo:` line. This will make it easier for
+ maintainers to see and edit.
+#. Use an existing standardized description of a parameter that appears
+ elsewhere in this project's documentation whenever possible. For example,
+ `request` is used as a parameter throughout the project with the description
+ "OAuthlib request." - there is no reason to describe it differently in your
+ function. Parameter descriptions should be a sentence that ends with a
+ period - even if it is just two words.
+#. When possible, include a `type` declaration for the parameter. For example,
+ a "request" param is often accompanied with `:type request: oauthlib.common.Request`.
+ The type is expected to be an object type reference, and should never end
+ in a period.
+#. If there is a textual docstring (recommended), use a single blank line to
+ separate the docstring and the params.
+#. When you cite class functions, please use backticks.
+
+Consolidated example
+
+ def foo(self, request, client, bar=None, key=None):
+ """
+ This method checks the `key` against the `client`. The `request` is
+ passed to maintain context.
+
+ Example MAC Authorization header, linebreaks added for clarity
+
+ Authorization: MAC id="h480djs93hd8",
+ nonce="1336363200:dj83hs9s",
+ mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
+
+ .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param bar:
+ :param key: MAC given provided by token endpoint.
+ """
+
How pull requests are checked, tested, and done
===============================================
First we pull the code into a local branch::
- git remote add <submitter-github-name> git@github.com:<submitter-github-name>/opencomparison.git
+ git remote add <submitter-github-name> git@github.com:<submitter-github-name>/oauthlib.git
git fetch <submitter-github-name>
git checkout -b <branch-name> <submitter-github-name>/<branch-name>
Then we run the tests::
- python -m unittest discover
+ py.test
We finish with a non-fastforward merge (to preserve the branch history) and push
to GitHub::
@@ -223,5 +311,5 @@ to GitHub::
git push upstream master
.. _installation: install.html
-.. _GitHub project: https://github.com/idan/oauthlib
-.. _issue tracker: https://github.com/idan/oauthlib/issues
+.. _GitHub project: https://github.com/oauthlib/oauthlib
+.. _issue tracker: https://github.com/oauthlib/oauthlib/issues
diff --git a/docs/faq.rst b/docs/faq.rst
index 4d896f5..d9cd5c6 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -14,19 +14,23 @@ What parts of OAuth 1 & 2 are supported?
OAuth 1 with RSA-SHA1 signatures says "could not import cryptography". What should I do?
----------------------------------------------------------------------------------
- Install cryptography via pip.
+ Install oauthlib with rsa flag or install cryptography manually via pip.
.. code-block:: sh
+ $ pip install oauthlib[rsa]
+ ..or..
$ pip install cryptography
OAuth 2 ServiceApplicationClient and OAuth 1 with RSA-SHA1 signatures say "could not import jwt". What should I do?
-------------------------------------------------------------------------------------------------------------------
- Install pyjwt and cryptography with pip.
+ Install oauthlib with signedtoken flag or install pyjwt and cryptography manually with pip.
.. code-block:: sh
+ $ pip install oauthlib[signedtoken]
+ ..or..
$ pip install pyjwt cryptography
What does ValueError `Only unicode objects are escapable. Got one of type X.` mean?
@@ -65,10 +69,17 @@ How do I use OAuthLib with Google, Twitter and other providers?
How do I use OAuthlib as a provider with Django, Flask and other web frameworks?
--------------------------------------------------------------------------------
- Providers using Django should seek out `django-oauth-toolkit`_
- and those using Flask `flask-oauthlib`_. For other frameworks,
- please get in touch by opening a `GitHub issue`_, on `G+`_ or
- on IRC #oauthlib irc.freenode.net.
+ Providers can be implemented in any web frameworks. However, some of
+ them have ready-to-use libraries to help integration:
+ - Django `django-oauth-toolkit`_
+ - Flask `flask-oauthlib`_
+ - Pyramid `pyramid-oauthlib`_
+ - Bottle `bottle-oauthlib`_
+
+ For other frameworks, please get in touch by opening a `GitHub issue`_ or
+ on `Gitter OAuthLib community`_. If you have written an OAuthLib package that
+ supports your favorite framework, please open a Pull Request to update the docs.
+
What is the difference between authentication and authorization?
----------------------------------------------------------------
@@ -91,6 +102,8 @@ Some argue OAuth 2 is worse than 1, is that true?
.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib
.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit
.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib
-.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new
-.. _`G+`: https://plus.google.com/communities/101889017375384052571
-.. _`difference`: http://www.cyberciti.biz/faq/authentication-vs-authorization/
+.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
+.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
+.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new
+.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby
+.. _`difference`: https://www.cyberciti.biz/faq/authentication-vs-authorization/
diff --git a/docs/feature_matrix.rst b/docs/feature_matrix.rst
index 0f9021d..df8cb0e 100644
--- a/docs/feature_matrix.rst
+++ b/docs/feature_matrix.rst
@@ -7,20 +7,33 @@ Extensions and variations that are outside the spec are not supported.
- HMAC-SHA1, RSA-SHA1 and plaintext signatures.
- Signature placement in header, url or body.
-OAuth 2 client and provider support for
-
-- Authorization Code Grant
-- Implicit Grant
-- Client Credentials Grant
-- Resource Owner Password Credentials Grant
-- Refresh Tokens
-- Bearer Tokens
-- Draft MAC tokens
-- Token Revocation
-- OpenID Connect Authentication
-
-with support for SAML2 and JWT tokens, dynamic client registration and more to
-come.
+OAuth 2.0 client and provider support for:
+
+- `RFC6749#section-4.1`_: Authorization Code Grant
+- `RFC6749#section-4.2`_: Implicit Grant
+- `RFC6749#section-4.3`_: Resource Owner Password Credentials Grant
+- `RFC6749#section-4.4`_: Client Credentials Grant
+- `RFC6749#section-6`_: Refresh Tokens
+- `RFC6750`_: Bearer Tokens
+- `RFC7009`_: Token Revocation
+- `RFC Draft MAC tokens`_
+- OAuth2.0 Provider: `OpenID Connect Core`_
+- OAuth2.0 Provider: `RFC7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
+- OAuth2.0 Provider: `RFC7662`_: Token Introspection
+- OAuth2.0 Provider: `RFC8414`_: Authorization Server Metadata
+
+Features to be implemented (any help/PR are welcomed):
+
+- OAuth2.0 **Client**: `OpenID Connect Core`_
+- OAuth2.0 **Client**: `RFC7636`_: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
+- OAuth2.0 **Client**: `RFC7662`_: Token Introspection
+- OAuth2.0 **Client**: `RFC8414`_: Authorization Server Metadata
+- SAML2
+- Bearer JWT as Client Authentication
+- Dynamic client registration
+- OpenID Discovery
+- OpenID Session Management
+- ...and more
Supported platforms
-------------------
@@ -31,3 +44,15 @@ should be able to use OAuthLib on any platform that supports Python. If you use
RSA you are limited to the platforms supported by `cryptography`_.
.. _`cryptography`: https://cryptography.io/en/latest/installation/
+.. _`RFC6749#section-4.1`: https://tools.ietf.org/html/rfc6749#section-4.1
+.. _`RFC6749#section-4.2`: https://tools.ietf.org/html/rfc6749#section-4.2
+.. _`RFC6749#section-4.3`: https://tools.ietf.org/html/rfc6749#section-4.3
+.. _`RFC6749#section-4.4`: https://tools.ietf.org/html/rfc6749#section-4.4
+.. _`RFC6749#section-6`: https://tools.ietf.org/html/rfc6749#section-6
+.. _`RFC6750`: https://tools.ietf.org/html/rfc6750
+.. _`RFC Draft MAC tokens`: https://tools.ietf.org/id/draft-ietf-oauth-v2-http-mac-02.html
+.. _`RFC7009`: https://tools.ietf.org/html/rfc7009
+.. _`RFC7662`: https://tools.ietf.org/html/rfc7662
+.. _`RFC7636`: https://tools.ietf.org/html/rfc7636
+.. _`OpenID Connect Core`: https://openid.net/specs/openid-connect-core-1_0.html
+.. _`RFC8414`: https://tools.ietf.org/html/rfc8414
diff --git a/docs/index.rst b/docs/index.rst
index 1699068..b6ce191 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -7,14 +7,14 @@ Welcome to OAuthLib's documentation!
====================================
If you can't find what you need or have suggestions for improvement, don't
-hesitate to open a `new issue on GitHub`_!
+hesitate to open a `new issue on GitHub`_!
Check out :doc:`error_reporting` for details on how to be an awesome bug reporter.
-For news and discussions please head over to our `G+ OAuthLib community`_.
+For news and discussions please head over to our `Gitter OAuthLib community`_.
-.. _`new issue on GitHub`: https://github.com/idan/oauthlib/issues/new
-.. _`G+ OAuthLib community`: https://plus.google.com/communities/101889017375384052571
+.. _`new issue on GitHub`: https://github.com/oauthlib/oauthlib/issues/new
+.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby
.. toctree::
:maxdepth: 1
diff --git a/docs/installation.rst b/docs/installation.rst
index 5a8b2cb..72d7b08 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -9,7 +9,7 @@ For various reasons you may wish to install using your OS packaging system and
install instructions for a few are shown below. Please send a PR to add a
missing one.
-Latest release on PYPI
+Latest release on PyPI
----------------------
@@ -22,7 +22,7 @@ Bleeding edge from GitHub master
.. code-block:: bash
- pip install -e git+https://github.com/idan/oauthlib.git#egg=oauthlib
+ pip install -e git+https://github.com/oauthlib/oauthlib.git#egg=oauthlib
Debian and derivatives like Ubuntu, Mint, etc.
---------------------------------------------
diff --git a/docs/oauth1/client.rst b/docs/oauth1/client.rst
index 741374e..ec6bdd7 100644
--- a/docs/oauth1/client.rst
+++ b/docs/oauth1/client.rst
@@ -52,15 +52,23 @@ Using the Client
**Request body**
The OAuth 1 spec only covers signing of x-www-url-formencoded information.
- If you are sending some other kind of data in the body (say, multipart file
- uploads), these don't count as a body for the purposes of signing. Don't
- provide the body to Client.sign() if it isn't x-www-url-formencoded data.
For convenience, you can pass body data in one of three ways:
* a dictionary
* an iterable of 2-tuples
* a properly-formatted x-www-url-formencoded string
+
+ If you are sending some other kind of data in the body, an additional
+ `oauth_body_hash` parameter will be included with the request. This parameter
+ provides an integrity check on non-formencoded request bodies.
+
+ *IMPORTANT* This extension is forward compatible: Service Providers that
+ have not implemented this extension can verify requests sent by Consumers
+ that have implemented this extension. If the Service Provider implements
+ this specification the integrity of the body is guaranteed. If the
+ Service Provider does not check body signatures, the remainder of the
+ request will still validate using the OAuth Core signature algorithm.
**RSA Signatures**
diff --git a/docs/oauth1/preconfigured_servers.rst b/docs/oauth1/preconfigured_servers.rst
index 7f7f386..b32e1ab 100644
--- a/docs/oauth1/preconfigured_servers.rst
+++ b/docs/oauth1/preconfigured_servers.rst
@@ -12,7 +12,7 @@ Construction is simple, only import your validator and you are good to go::
server = WebApplicationServer(your_validator)
-All endpoints are documented in :doc:`endpoints`.
+All endpoints are documented in :doc:`Provider endpoints <endpoints/endpoints>`.
.. autoclass:: oauthlib.oauth1.WebApplicationServer
:members:
diff --git a/docs/oauth1/security.rst b/docs/oauth1/security.rst
index a1432a9..df1e2a0 100644
--- a/docs/oauth1/security.rst
+++ b/docs/oauth1/security.rst
@@ -16,11 +16,13 @@ A few important facts regarding OAuth security
* **Tokens must be random**, OAuthLib provides a method for generating
secure tokens and it's packed into ``oauthlib.common.generate_token``,
- use it. If you decide to roll your own, use ``random.SystemRandom``
- which is based on ``os.urandom`` rather than the default ``random``
- based on the effecient but not truly random Mersenne Twister.
- Predictable tokens allow attackers to bypass virtually all defences
- OAuth provides.
+ use it. If you decide to roll your own, use ``secrets.SystemRandom``
+ for Python 3.6 and later. The ``secrets`` module is designed for
+ generating cryptographically strong random numbers. For earlier versions
+ of Python, use ``random.SystemRandom`` which is based on ``os.urandom``
+ rather than the default ``random`` based on the effecient but not truly
+ random Mersenne Twister. Predictable tokens allow attackers to bypass
+ virtually all defences OAuth provides.
* **Timing attacks are real** and more than possible if you host your
application inside a shared datacenter. Ensure all ``validate_`` methods
diff --git a/docs/oauth1/server.rst b/docs/oauth1/server.rst
index f254c91..db469d2 100644
--- a/docs/oauth1/server.rst
+++ b/docs/oauth1/server.rst
@@ -433,10 +433,10 @@ shown below as well as run your flask server locally on port `5000`.
7. Let us know how it went!
---------------------------
-Drop a line in our `G+ community`_ or open a `GitHub issue`_ =)
+Drop a line in our `Gitter OAuthLib community`_ or open a `GitHub issue`_ =)
-.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
-.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new
+.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby
+.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new
If you run into issues it can be helpful to enable debug logging::
diff --git a/docs/oauth2/clients/client.rst b/docs/oauth2/clients/client.rst
index 11da2cc..9a5a4ff 100644
--- a/docs/oauth2/clients/client.rst
+++ b/docs/oauth2/clients/client.rst
@@ -24,5 +24,5 @@ to use them please browse the documentation for each client type below.
If you are interested in integrating OAuth 2 support into your favourite
HTTP library you might find the requests-oauthlib implementation interesting.
- .. _`requests`: https://github.com/kennethreitz/requests
+ .. _`requests`: https://github.com/requests/requests
.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib
diff --git a/docs/oauth2/endpoints/endpoints.rst b/docs/oauth2/endpoints/endpoints.rst
index 0e70798..0dd2da0 100644
--- a/docs/oauth2/endpoints/endpoints.rst
+++ b/docs/oauth2/endpoints/endpoints.rst
@@ -10,20 +10,26 @@ certain users resources to a client, to supply said client with a token
embodying this authorization and to verify that the token is valid when the
client attempts to access the user resources on their behalf.
+
.. toctree::
:maxdepth: 2
authorization
+ introspect
token
- resource
+ metadata
revocation
+ resource
-There are three different endpoints, the authorization endpoint which mainly
+There are three main endpoints, the authorization endpoint which mainly
handles user authorization, the token endpoint which provides tokens and the
resource endpoint which provides access to protected resources. It is to the
endpoints you will feed requests and get back an almost complete response. This
process is simplified for you using a decorator such as the django one described
-later.
+later (but it's applicable to all other web frameworks libraries).
The main purpose of the endpoint in OAuthLib is to figure out which grant type
or token to dispatch the request to.
+
+Then, you can extend your OAuth implementation by proposing introspect,
+revocation and/or providing metadata endpoints.
diff --git a/docs/oauth2/endpoints/introspect.rst b/docs/oauth2/endpoints/introspect.rst
new file mode 100644
index 0000000..53ade8b
--- /dev/null
+++ b/docs/oauth2/endpoints/introspect.rst
@@ -0,0 +1,26 @@
+===================
+Token introspection
+===================
+
+Introspect endpoints read opaque access and/or refresh tokens upon client
+request. Also known as tokeninfo.
+
+.. code-block:: python
+
+ # Initial setup
+ from your_validator import your_validator
+ server = WebApplicationServer(your_validator)
+
+ # Token revocation
+ uri = 'https://example.com/introspect'
+ headers, body, http_method = {}, 'token=sldafh309sdf', 'POST'
+
+ headers, body, status = server.create_introspect_response(uri,
+ headers=headers, body=body, http_method=http_method)
+
+ from your_framework import http_response
+ http_response(body, status=status, headers=headers)
+
+
+.. autoclass:: oauthlib.oauth2.IntrospectEndpoint
+ :members:
diff --git a/docs/oauth2/endpoints/metadata.rst b/docs/oauth2/endpoints/metadata.rst
new file mode 100644
index 0000000..d44e8b7
--- /dev/null
+++ b/docs/oauth2/endpoints/metadata.rst
@@ -0,0 +1,72 @@
+===================
+Metadata endpoint
+===================
+
+OAuth2.0 Authorization Server Metadata (`RFC8414`_) endpoint provide the metadata of your authorization server. Since the metadata results can be a combination of OAuthlib's Endpoint (see :doc:`preconfigured_servers`), the MetadataEndpoint's class takes a list of Endpoints in parameter, and aggregate the metadata in the response.
+
+See below an example of usage with `bottle-oauthlib`_ when using a `LegacyApplicationServer` (password grant) endpoint:
+
+.. code-block:: python
+
+ import bottle
+ from bottle_oauthlib.oauth2 import BottleOAuth2
+ from oauthlib import oauth2
+
+ app = bottle.Bottle()
+ app.authmetadata = BottleOAuth2(app)
+
+ oauthlib_server = oauth2.LegacyApplicationServer(oauth2.RequestValidator())
+ app.authmetadata.initialize(oauth2.MetadataEndpoint([oauthlib_server], claims={
+ "issuer": "https://xx",
+ "token_endpoint": "https://xx/token",
+ "revocation_endpoint": "https://xx/revoke",
+ "introspection_endpoint": "https://xx/tokeninfo"
+ }))
+
+
+ @app.get('/.well-known/oauth-authorization-server')
+ @app.authmetadata.create_metadata_response()
+ def metadata():
+ pass
+
+
+ if __name__ == "__main__":
+ app.run() # pragma: no cover
+
+
+Sample response's output:
+
+
+.. code-block:: javascript
+
+ $ curl -s http://localhost:8080/.well-known/oauth-authorization-server|jq .
+ {
+ "issuer": "https://xx",
+ "token_endpoint": "https://xx/token",
+ "revocation_endpoint": "https://xx/revoke",
+ "introspection_endpoint": "https://xx/tokeninfo",
+ "grant_types_supported": [
+ "password",
+ "refresh_token"
+ ],
+ "token_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ],
+ "revocation_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ],
+ "introspection_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ]
+ }
+
+
+.. autoclass:: oauthlib.oauth2.MetadataEndpoint
+ :members:
+
+
+.. _`RFC8414`: https://tools.ietf.org/html/rfc8414
+.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthli
diff --git a/docs/oauth2/grants/jwt.rst b/docs/oauth2/grants/jwt.rst
index 87aed11..2c1c0e2 100644
--- a/docs/oauth2/grants/jwt.rst
+++ b/docs/oauth2/grants/jwt.rst
@@ -1,7 +1,14 @@
-==========
-JWT Tokens
-==========
+==============================================================
+JWT Profile for Client Authentication and Authorization Grants
+==============================================================
-Not yet implemented. Track progress in `GitHub issue 50`_.
+If you're looking at JWT Tokens, please see :doc:`Bearer Tokens </oauth2/tokens/bearer>` instead.
-.. _`GitHub issue 50`: https://github.com/idan/oauthlib/issues/50
+The JWT Profile `RFC7523`_ implements the `RFC7521`_ abstract assertion
+protocol. It aims to extend the OAuth2 protocol to use JWT as an
+additional authorization grant.
+
+Currently, this is not implemented but all PRs are welcome. See how to :doc:`Contribute </contributing>`.
+
+.. _`RFC7521`: https://tools.ietf.org/html/rfc7521
+.. _`RFC7523`: https://tools.ietf.org/html/rfc7523
diff --git a/docs/oauth2/oauth2provider-legend.dot b/docs/oauth2/oauth2provider-legend.dot
new file mode 100644
index 0000000..ad87d80
--- /dev/null
+++ b/docs/oauth2/oauth2provider-legend.dot
@@ -0,0 +1,32 @@
+digraph oauthlib_legend {
+
+ subgraph cluster_legend {
+ label="Legend";
+
+ /*
+ method [ shape=record; label="{{RequestValidator\nmethod name|arguments}|return values}" ];
+ endpoint [ shape=record; label="{Endpoint name|{function name|arguments}|grant type}" ];
+ webframework [ shape=hexagon; label="Upstream functions" ];
+ */
+
+ flow_code_token [shape=none,label="Authorization Code\nAccess Token Request"];
+ flow_code_auth [shape=none,label="Authorization Code\nAuthorization Request"];
+ flow_implicit [shape=none,label="Implicit Grant"];
+ flow_password [shape=none,label="Resource Owner Password\nCredentials Grant"];
+ flow_clicreds [shape=none,label="Client Credentials Grant"];
+ flow_refresh [shape=none,label="Refresh Grant"];
+ flow_introspect [shape=none,label="Token Introspection"];
+ flow_revoke [shape=none,label="Token Revoke"];
+ flow_resource [shape=none,label="Resource Access"];
+ flow_code_token -> a [style=bold,color=darkgreen];
+ flow_code_auth -> b [style=bold,color=green];
+ flow_implicit -> c [style=bold,color=orange];
+ flow_password -> d [style=bold,color=red];
+ flow_clicreds -> e [style=bold,color=blue];
+ flow_refresh -> f [style=bold,color=brown];
+ flow_introspect -> g [style=bold,color=yellow];
+ flow_revoke -> h [style=bold,color=purple];
+ flow_resource -> i [style=bold,color=pink];
+ a, b, c, d, e, f, g, h, i [shape=none,label=""];
+ }
+}
diff --git a/docs/oauth2/oauth2provider-server.dot b/docs/oauth2/oauth2provider-server.dot
new file mode 100644
index 0000000..ec24078
--- /dev/null
+++ b/docs/oauth2/oauth2provider-server.dot
@@ -0,0 +1,268 @@
+digraph oauthlib {
+ /* Naming conventions:
+ f_ : functions in shape=record
+ endpoint_ : endpoints in shape=record
+ webapi_ : oauthlib entry/exit points in shape=hexagon
+ if_ : internal conditions
+ r_ : used when returning from two functions into one for improving clarity
+ */
+ center="1"
+ edge [ style=bold ];
+
+ /* Web Framework Entry and Exit points */
+ {
+ node [ shape=hexagon ];
+ edge [ style=normal ];
+
+ webapi_request [ label="WebFramework\nHTTP request" ];
+ webapi_request:s ->
+ endpoint_authorize:top:n,
+ endpoint_token:top:n,
+ endpoint_introspect:top:n,
+ endpoint_revoke:top:n,
+ endpoint_resource:top:n;
+ webapi_response [ label="WebFramework\nHTTP response" ];
+ }
+
+ /* OAuthlib Endpoints */
+ {
+ rank=same;
+
+ endpoint_authorize [ shape=record; label="{<top>Authorize Endpoint|{create_authorize_response|{uri|method|body|headers|credentials}}|{<token>token|<code>code}}" ];
+ endpoint_token [ shape=record; label="{<top>Token Endpoint|{create_token_response|{uri|method|body|headers|credentials}}|{<authorization_code>authorization_code|<password>password|<client_credentials>client_credentials|<refresh_token>refresh_token}}" ];
+ endpoint_revoke [ shape=record; label="{<top>Revocation Endpoint|{create_revocation_response|{uri|method|body|headers}}}" ];
+ endpoint_introspect [ shape=record; label="{<top>Introspect Endpoint|{create_introspect_response|{uri|method|body|headers}}}" ];
+ endpoint_resource [ shape=record; label="{<top>Resource Endpoint|{verify_request|{uri|method|body|headers|scopes_list}}}" ];
+ }
+
+ /* OAuthlib RequestValidator Methods */
+ {
+ node [ shape=record ];
+
+ f_client_authentication_required [ label="{{<top>client_authentication_required|request}|{<true>True|<false>False}}"; ];
+ f_authenticate_client [ label="{{<top>authenticate_client|request}|{<true>True|<false>False}}";];
+ f_authenticate_client_id [ label="{{<top>authenticate_client_id|{client_id|request}}|{<true>True|<false>False}}"; ];
+ f_validate_grant_type [ label="{{<top>validate_grant_type|{client_id|grant_type|client|request}}|{<true>True|<false>False}}"; ];
+ f_validate_code [ label="{{<top>validate_code|{client_id|code|request}}|{<true>True|<false>False}}"; ];
+ f_confirm_redirect_uri [ label="{{<top>confirm_redirect_uri|{client_id|code|redirect_uri|client|request}}|{<true>True|<false>False}}"; ];
+ f_get_default_redirect_uri [ label="{{<top>get_default_redirect_uri|{client_id|request}}|{<redirect_uri>redirect_uri|<none>None}}"; ];
+ f_invalidate_authorization_code [ label="{{<top>invalidate_authorization_code|{client_id|code|request}}|None}"; ];
+ f_validate_scopes [ label="{{<top>validate_scopes|{client_id|scopes|client|request}}|{<true>True|<false>False}}"; ];
+ f_save_bearer_token [ label="{{<top>save_bearer_token|{token|request}}|None}"; ];
+ f_revoke_token [ label="{{<top>revoke_token|{token|token_type_hint|request}}|None}"; ];
+ f_validate_client_id [ label="{{<top>validate_client_id|{client_id|request}}|{<true>True|<false>False}}"; ];
+ f_validate_redirect_uri [ label="{{<top>validate_redirect_uri|{client_id|redirect_uri|request}}|{<true>True|<false>False}}"; ];
+ f_is_pkce_required [ label="{{<top>is_pkce_required|{client_id|request}}|{<true>True|<false>False}}"; ];
+ f_validate_response_type [ label="{{<top>validate_response_type|{client_id|response_type|client|request}}|{<true>True|<false>False}}"; ];
+ f_save_authorization_code [ label="{{<top>save_authorization_code|{client_id|code|request}}|None}"; ];
+ f_validate_bearer_token [ label="{{<top>validate_bearer_token|{token|scopes|request}}|{<true>True|<false>False}}"; ];
+ f_validate_refresh_token [ label="{{<top>validate_refresh_token|{refresh_token|client|request}}|{<true>True|<false>False}}"; ];
+ f_get_default_scopes [ label="{{<top>get_default_scopes|{client_id|request}}|{<scopes>[scopes]}}"; ];
+ f_get_original_scopes [ label="{{<top>get_original_scopes|{refresh_token|request}}|{<scopes>[scopes]}}"; ];
+ f_is_within_original_scope [ label="{{<top>is_within_original_scope|{refresh_scopes|refresh_token|request}}|{<true>True|<false>False}}"; ];
+ f_validate_user [ label="{{<top>validate_user|{username|password|client|request}}|{<true>True|<false>False}}"; ];
+ f_introspect_token [ label="{{<top>introspect_token|{token|token_type_hint|request}}|{<claims>\{claims\}|<none>None}}"; ];
+ }
+
+ /* OAuthlib Conditions */
+
+ if_code_challenge [ label="if code_challenge"; ];
+ if_redirect_uri [ label="if redirect_uri"; ];
+ if_redirect_uri_present [ shape=none;label="present"; ];
+ if_redirect_uri_missing [ shape=none;label="missing"; ];
+ if_scopes [ label="if scopes"; ];
+ if_all [ label="all(request_scopes not in scopes)"; ];
+
+ /* OAuthlib functions returns helpers */
+ r_client_authenticated [ shape=none,label="client authenticated"; ];
+
+ /* OAuthlib errors */
+ e_normal [ shape=none,label="ERROR" ];
+
+ /* Ranking by functional roles */
+ {
+ rank = same;
+ f_validate_client_id;
+ f_validate_code;
+ /* f_validate_user; */
+ f_validate_bearer_token;
+ f_validate_refresh_token;
+ f_introspect_token;
+ f_revoke_token;
+ }
+ {
+ rank = same;
+ f_validate_redirect_uri;
+ f_confirm_redirect_uri;
+ }
+ {
+ rank = same;
+ f_save_bearer_token;
+ f_save_authorization_code;
+ }
+ {
+ rank = same;
+ f_invalidate_authorization_code;
+ }
+ {
+ rank = same;
+ f_validate_scopes;
+ f_get_original_scopes;
+ f_get_default_scopes;
+ }
+ {
+ rank = same;
+ f_is_within_original_scope;
+ }
+
+ /* Authorization Code - Access Token Request */
+ {
+ edge [ color=darkgreen ];
+
+ endpoint_token:authorization_code:s -> f_client_authentication_required;
+ f_client_authentication_required:true:s -> f_authenticate_client;
+ f_client_authentication_required:false:s -> f_authenticate_client_id;
+ f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ];
+ f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ];
+ r_client_authenticated -> f_validate_grant_type;
+ f_validate_grant_type:true:s -> f_validate_code;
+
+ f_validate_code:true:s -> if_redirect_uri;
+ if_redirect_uri -> if_redirect_uri_present [ arrowhead=none ];
+ if_redirect_uri -> if_redirect_uri_missing [ arrowhead=none ];
+ if_redirect_uri_present -> f_confirm_redirect_uri;
+ if_redirect_uri_missing -> f_get_default_redirect_uri;
+ f_get_default_redirect_uri:redirect_uri:s -> f_confirm_redirect_uri;
+
+ f_confirm_redirect_uri:true:s -> f_save_bearer_token;
+ f_save_bearer_token -> f_invalidate_authorization_code;
+ f_invalidate_authorization_code -> webapi_response;
+ }
+ /* Authorization Code - Authorization Request */
+ {
+ edge [ color=green ];
+
+ endpoint_authorize:code:s -> f_validate_client_id;
+ f_validate_client_id:true:s -> if_redirect_uri;
+ if_redirect_uri -> if_redirect_uri_present [ arrowhead=none ];
+ if_redirect_uri -> if_redirect_uri_missing [ arrowhead=none ];
+ if_redirect_uri_present -> f_validate_redirect_uri;
+ if_redirect_uri_missing -> f_get_default_redirect_uri;
+
+ f_validate_redirect_uri:true:s -> f_validate_response_type;
+ f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type;
+ f_validate_response_type:true:s -> f_is_pkce_required;
+ f_is_pkce_required:true:s -> if_code_challenge;
+ f_is_pkce_required:false:s -> f_validate_scopes;
+
+ if_code_challenge -> f_validate_scopes [ label="present" ];
+ if_code_challenge -> e_normal [ label="missing",style=dashed ];
+
+ f_validate_scopes:true:s -> f_save_authorization_code;
+ f_save_authorization_code -> webapi_response;
+ }
+
+ /* Implicit */
+ {
+ edge [ color=orange ];
+
+ endpoint_authorize:token:s -> f_validate_client_id;
+ f_validate_client_id:true:s -> if_redirect_uri;
+ if_redirect_uri -> if_redirect_uri_present [ arrowhead=none ];
+ if_redirect_uri -> if_redirect_uri_missing [ arrowhead=none ];
+ if_redirect_uri_present -> f_validate_redirect_uri;
+ if_redirect_uri_missing -> f_get_default_redirect_uri;
+
+ f_validate_redirect_uri:true:s -> f_validate_response_type;
+ f_get_default_redirect_uri:redirect_uri:s -> f_validate_response_type;
+ f_validate_response_type:true:s -> f_validate_scopes;
+ f_validate_scopes:true:s -> f_save_bearer_token;
+ f_save_bearer_token -> webapi_response;
+ }
+
+ /* Resource Owner Password Grant */
+ {
+ edge [ color=red ];
+
+ endpoint_token:password:s -> f_client_authentication_required;
+ f_client_authentication_required:true:s -> f_authenticate_client;
+ f_client_authentication_required:false:s -> f_authenticate_client_id;
+ f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ];
+ f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ];
+ r_client_authenticated -> f_validate_user;
+ f_validate_user:true:s -> f_validate_grant_type;
+
+ f_validate_grant_type:true:s -> if_scopes;
+ if_scopes -> f_validate_scopes [ label="present" ];
+ if_scopes -> f_get_default_scopes [ label="missing" ];
+
+ f_validate_scopes:true:s -> f_save_bearer_token;
+ f_get_default_scopes -> f_save_bearer_token;
+ f_save_bearer_token -> webapi_response;
+ }
+
+ /* Client Credentials Grant */
+ {
+ edge [ color=blue ];
+
+ endpoint_token:client_credentials:s -> f_authenticate_client;
+ f_authenticate_client:true:s -> f_validate_grant_type;
+ f_validate_grant_type:true:s -> f_validate_scopes;
+ f_validate_scopes:true:s -> f_save_bearer_token;
+ f_save_bearer_token -> webapi_response;
+ }
+
+ /* Refresh Grant */
+ {
+ edge [ color=brown ];
+
+ endpoint_token:refresh_token:s -> f_client_authentication_required;
+ f_client_authentication_required:true:s -> f_authenticate_client;
+ f_client_authentication_required:false:s -> f_authenticate_client_id;
+ f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ];
+ f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ];
+ r_client_authenticated -> f_validate_grant_type;
+
+ f_validate_grant_type:true:s -> f_validate_refresh_token;
+ f_validate_refresh_token:true:s -> f_get_original_scopes;
+ f_get_original_scopes -> if_all;
+ if_all -> f_is_within_original_scope [ label="True" ];
+ if_all -> f_save_bearer_token [ label="False" ];
+ f_is_within_original_scope:true:s -> f_save_bearer_token;
+ f_save_bearer_token -> webapi_response;
+ }
+
+ /* Introspect Endpoint */
+ {
+ edge [ color=yellow ];
+
+ endpoint_introspect:s -> f_client_authentication_required;
+ f_client_authentication_required:true:s -> f_authenticate_client;
+ f_client_authentication_required:false:s -> f_authenticate_client_id;
+ f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ];
+ f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ];
+ r_client_authenticated -> f_introspect_token;
+ f_introspect_token:claims -> webapi_response;
+ }
+
+ /* Revocation Endpoint */
+ {
+ edge [ color=purple ];
+
+ endpoint_revoke:s -> f_client_authentication_required;
+ f_client_authentication_required:true:s -> f_authenticate_client;
+ f_client_authentication_required:false:s -> f_authenticate_client_id;
+ f_authenticate_client:true:s -> r_client_authenticated [ arrowhead=none ];
+ f_authenticate_client_id:true:s -> r_client_authenticated [ arrowhead=none ];
+ r_client_authenticated -> f_revoke_token;
+ f_revoke_token:s -> webapi_response;
+ }
+
+ /* Resource Access - Verify Request */
+ {
+ edge [ color=pink ];
+
+ endpoint_resource:s -> f_validate_bearer_token;
+ f_validate_bearer_token:true -> webapi_response;
+ }
+}
diff --git a/docs/oauth2/oidc/id_tokens.rst b/docs/oauth2/oidc/id_tokens.rst
index 5d6aa91..999cfa7 100644
--- a/docs/oauth2/oidc/id_tokens.rst
+++ b/docs/oauth2/oidc/id_tokens.rst
@@ -5,7 +5,9 @@ The creation of `ID Tokens`_ is ultimately done not by OAuthLib but by your ``Re
content is dependent on your implementation of users, their attributes, any claims you may wish to support, as well as the
details of how you model the notion of a Client Application. As such OAuthLib simply calls your validator's ``get_id_token``
method at the appropriate times during the authorization flow, depending on the grant type requested (Authorization Code, Implicit,
-Hybrid, etc.)
+Hybrid, etc.).
+
+See examples below.
.. _`ID Tokens`: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
@@ -13,4 +15,35 @@ Hybrid, etc.)
:members: get_id_token
+JWT/JWS example with pyjwt library
+----------------------------------
+
+An example below using Cryptography library to load the private key and PyJWT to sign the JWT.
+Note that the claims list in the "data" dict must be set accordingly to the auth request.
+
+You can switch to jwcrypto library if you want to return JWE instead.
+
+.. code-block:: python
+
+ class MyValidator(RequestValidator):
+ def __init__(self, **kwargs):
+ with open(path.join(path.dirname(path.realpath(__file__)), "./id_rsa"), 'rb') as fd:
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import serialization
+ self.private_pem = serialization.load_pem_private_key(
+ fd.read(),
+ password=None,
+ backend=default_backend()
+ )
+
+ super().__init__(self, **kwargs)
+
+ def get_id_token(self, token, token_handler, request):
+ import jwt
+
+ data = {"nonce": request.nonce} if request.nonce is not None else {}
+
+ for claim_key in request.claims:
+ data[claim_key] = request.userattributes[claim_key] # this must be set in another callback
+ return jwt.encode(data, self.private_pem, 'RS256')
diff --git a/docs/oauth2/oidc/validator.rst b/docs/oauth2/oidc/validator.rst
index c92b726..a03adfe 100644
--- a/docs/oauth2/oidc/validator.rst
+++ b/docs/oauth2/oidc/validator.rst
@@ -1,7 +1,28 @@
-RequestValidator Extensions
-============================
+OpenID Connect
+=========================================
-Four methods must be implemented in your validator subclass if you wish to support OpenID Connect:
+Migrate your OAuth2.0 server into an OIDC provider
+----------------------------------------------------
+
+If you have a OAuth2.0 provider running and want to upgrade to OIDC, you can
+upgrade it by replacing one line of code:
+
+.. code-block:: python
+
+ from oauthlib.oauth2 import Server
+
+Into
+
+.. code-block:: python
+
+ from oauthlib.openid import Server
+
+Then, you have to implement the new RequestValidator methods as shown below.
+
+RequestValidator Extension
+----------------------------------------------------
+
+A couple of methods must be implemented in your validator subclass if you wish to support OpenID Connect:
.. autoclass:: oauthlib.oauth2.RequestValidator
- :members: validate_silent_authorization, validate_silent_login, validate_user_match, get_id_token
+ :members: validate_silent_authorization, validate_silent_login, validate_user_match, get_id_token, get_authorization_code_scopes, validate_jwt_bearer_token
diff --git a/docs/oauth2/preconfigured_servers.rst b/docs/oauth2/preconfigured_servers.rst
index 6184c27..e1f629c 100644
--- a/docs/oauth2/preconfigured_servers.rst
+++ b/docs/oauth2/preconfigured_servers.rst
@@ -12,7 +12,8 @@ Construction is simple, only import your validator and you are good to go::
server = WebApplicationServer(your_validator)
-If you prefer to construct tokens yourself you may pass a token generator::
+If you prefer to construct tokens yourself you may pass a token generator (see
+ :doc:`Tokens <tokens/tokens>` for more examples like JWT) ::
def your_token_generator(request, refresh_token=False):
return 'a_custom_token' + request.client_id
@@ -21,6 +22,9 @@ If you prefer to construct tokens yourself you may pass a token generator::
This function is passed the request object and a boolean indicating whether to generate an access token (False) or a refresh token (True).
+.. autoclass:: oauthlib.oauth2.Server
+ :members:
+
.. autoclass:: oauthlib.oauth2.WebApplicationServer
:members:
diff --git a/docs/oauth2/server.rst b/docs/oauth2/server.rst
index 9d6b502..dad0aae 100644
--- a/docs/oauth2/server.rst
+++ b/docs/oauth2/server.rst
@@ -6,8 +6,10 @@ OAuthLib is a dependency free library that may be used with any web
framework. That said, there are framework specific helper libraries
to make your life easier.
-- For Django there is `django-oauth-toolkit`_.
-- For Flask there is `flask-oauthlib`_.
+- Django `django-oauth-toolkit`_
+- Flask `flask-oauthlib`_
+- Pyramid `pyramid-oauthlib`_
+- Bottle `bottle-oauthlib`_
If there is no support for your favourite framework and you are interested
in providing it then you have come to the right place. OAuthLib can handle
@@ -17,11 +19,23 @@ as well as provide an interface for a backend to store tokens, clients, etc.
.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit
.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib
+.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
+.. _`bottle-oauthlib`: https://github.com/thomsonreuters/bottle-oauthlib
.. contents:: Tutorial Contents
:depth: 3
-1. Create your datastore models
+1. OAuth2.0 Provider flows
+-------------------------------
+
+OAuthLib interface between web framework and provider implementation are not always easy to follow, it's why a graph below has been done to better understand the implication of OAuthLib in the request's lifecycle.
+
+
+.. graphviz:: oauth2provider-legend.dot
+.. graphviz:: oauth2provider-server.dot
+
+
+2. Create your datastore models
-------------------------------
These models will represent various OAuth specific concepts. There are a few
@@ -242,7 +256,18 @@ the token.
expires_at = django.db.models.DateTimeField()
-2. Implement a validator
+**PKCE Challenge (optional)**
+
+ If you want to support PKCE, you have to associate a `code_challenge`
+ and a `code_challenge_method` to the actual Authorization Code.
+
+ .. code-block:: python
+
+ challenge = django.db.models.CharField(max_length=128)
+ challenge_method = django.db.models.CharField(max_length=6)
+
+
+3. Implement a validator
------------------------
The majority of the work involved in implementing an OAuth 2 provider
@@ -275,7 +300,7 @@ all methods depending on which grant types you wish to support. A skeleton
validator listing the methods required for the WebApplicationServer is
available in the `examples`_ folder on GitHub.
-.. _`examples`: https://github.com/idan/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py
+.. _`examples`: https://github.com/oauthlib/oauthlib/blob/master/examples/skeleton_oauth2_web_application_server.py
Relevant sections include:
@@ -286,7 +311,7 @@ Relevant sections include:
security
-3. Create your composite endpoint
+4. Create your composite endpoint
---------------------------------
Each of the endpoints can function independently from each other, however
@@ -311,7 +336,7 @@ Relevant sections include:
preconfigured_servers
-4. Create your endpoint views
+5. Create your endpoint views
-----------------------------
We are implementing support for the Authorization Code Grant and will
@@ -415,7 +440,7 @@ The example using Django but should be transferable to any framework.
return HttpResponseBadRequest('Evil client is unable to send a proper request. Error is: ' + e.description)
-5. Protect your APIs using scopes
+6. Protect your APIs using scopes
---------------------------------
Let's define a decorator we can use to protect the views.
@@ -486,13 +511,13 @@ at runtime by a function, rather then by a list.
# A view that has its views functionally set.
return HttpResponse('pictures of cats')
-6. Let us know how it went!
+7. Let us know how it went!
---------------------------
-Drop a line in our `G+ community`_ or open a `GitHub issue`_ =)
+Drop a line in our `Gitter OAuthLib community`_ or open a `GitHub issue`_ =)
-.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
-.. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new
+.. _`Gitter OAuthLib community`: https://gitter.im/oauthlib/Lobby
+.. _`GitHub issue`: https://github.com/oauthlib/oauthlib/issues/new
If you run into issues it can be helpful to enable debug logging.
diff --git a/docs/oauth2/tokens/bearer.rst b/docs/oauth2/tokens/bearer.rst
index 8c6270d..0776db8 100644
--- a/docs/oauth2/tokens/bearer.rst
+++ b/docs/oauth2/tokens/bearer.rst
@@ -2,12 +2,122 @@
Bearer Tokens
=============
-The most common OAuth 2 token type. It provides very little in terms of security
-and relies heavily upon the ability of the client to keep the token secret.
+The most common OAuth 2 token type.
-Bearer tokens are the default setting with all configured endpoints. Generally
+Bearer tokens is the default setting for all configured endpoints. Generally
you will not need to ever construct a token yourself as the provided servers
will do so for you.
+By default, :doc:`*Server </oauth2/preconfigured_servers>` generate Bearer tokens as
+random strings. However, you can change the default behavior to generate JWT
+instead. All preconfigured servers take as parameters `token_generator` and
+`refresh_token_generator` to fit your needs.
+
+.. contents:: Tutorial Contents
+ :depth: 3
+
+
+1. Generate signed JWT
+----------------------
+
+A function is available to generate signed JWT (with RS256 PEM key) with static
+and dynamic claims.
+
+.. code-block:: python
+
+ from oauthlib.oauth2.rfc6749 import tokens
+ from oauthlib.oauth2 import Server
+
+ private_pem_key = <load_your_key_in_pem_format>
+ validator = <instantiate_your_validator>
+
+ server = Server(
+ your_validator,
+ token_generator=tokens.signed_token_generator(private_pem_key, issuer="foobar")
+ )
+
+
+Note that you can add any custom claims in `RequestValidator` methods by adding them to
+`request.claims` dictionary. Example below:
+
+
+.. code-block:: python
+
+ def validate_client_id(self, client_id, request):
+ (.. your usual checks ..)
+
+ request.claims = {
+ 'aud': self.client_id
+ }
+ return True
+
+
+Once completed, the token endpoint will generate access_token in JWT form:
+
+.. code-block:: shell
+
+
+ access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJy(..)&expires_in=120&token_type=Bearer(..)
+
+
+And you will find all claims in its decoded form:
+
+
+.. code-block:: javascript
+
+ {
+ "aud": "<client_id>",
+ "iss": "foobar",
+ "scope": "profile calendar",
+ "exp": 12345,
+ }
+
+
+2. Define your own implementation (text, JWT, JWE, ...)
+----------------------------------------------------------------
+
+Sometime you may want to generate custom `access_token` with a reference from a
+database (as text) or use a HASH signature in JWT or use JWE (encrypted content).
+
+Also, note that you can declare the generate function in your instanciated
+validator to benefit of the `self` variables.
+
+See the example below:
+
+.. code-block:: python
+
+ class YourValidator(RequestValidator):
+ def __init__(self, secret, issuer):
+ self.secret = secret
+ self.issuer = issuer
+
+ def generate_access_token(self, request):
+ token = jwt.encode({
+ "ref": str(libuuid.uuid4()),
+ "aud": request.client_id,
+ "iss": self.issuer,
+ "exp": now + datetime.timedelta(seconds=request.expires_in)
+ }, self.secret, algorithm='HS256').decode()
+ return token
+
+
+Then associate it to your `Server`:
+
+.. code-block:: python
+
+ validator = YourValidator(secret="<your_secret>", issuer="<your_issuer_id>")
+
+ server = Server(
+ your_validator,
+ token_generator=validator.generate_access_token
+ )
+
+
+3. BearerToken API
+------------------
+
+If none of the :doc:`/oauth2/preconfigured_servers` fit your needs, you can
+declare your own Endpoints and use the `BearerToken` API as below.
+
.. autoclass:: oauthlib.oauth2.BearerToken
:members:
diff --git a/docs/oauth2/tokens/mac.rst b/docs/oauth2/tokens/mac.rst
index 4986819..afb6948 100644
--- a/docs/oauth2/tokens/mac.rst
+++ b/docs/oauth2/tokens/mac.rst
@@ -5,4 +5,4 @@ MAC tokens
Not yet implemented. Track progress in `GitHub issue 29`_. Might never be
supported depending on whether the work on the specification is resumed or not.
-.. _`GitHub issue 29`: https://github.com/idan/oauthlib/issues/29
+.. _`GitHub issue 29`: https://github.com/oauthlib/oauthlib/issues/29
diff --git a/docs/oauth2/tokens/saml.rst b/docs/oauth2/tokens/saml.rst
index 9a00937..5faf16a 100644
--- a/docs/oauth2/tokens/saml.rst
+++ b/docs/oauth2/tokens/saml.rst
@@ -4,4 +4,4 @@ SAML Tokens
Not yet implemented. Track progress in `GitHub issue 49`_.
-.. _`GitHub issue 49`: https://github.com/idan/oauthlib/issues/49
+.. _`GitHub issue 49`: https://github.com/oauthlib/oauthlib/issues/49
diff --git a/docs/oauth2/tokens/tokens.rst b/docs/oauth2/tokens/tokens.rst
index f0adc97..4e19e7e 100644
--- a/docs/oauth2/tokens/tokens.rst
+++ b/docs/oauth2/tokens/tokens.rst
@@ -3,8 +3,7 @@ Tokens
======
The main token type of OAuth 2 is Bearer tokens and that is what OAuthLib
-currently supports. Other tokens, such as JWT, SAML and possibly MAC (if the
-spec matures) can easily be added (and will be in due time).
+currently supports. Other tokens, such as SAML and MAC can easily be added.
The purpose of a token is to authorize access to protected resources to a client
(i.e. your G+ feed).
@@ -15,8 +14,8 @@ providers, notably Facebook, do not provide this information. Per the
is missing. You can force a ``MissingTokenTypeError`` exception instead, by
setting ``OAUTHLIB_STRICT_TOKEN_TYPE`` in the environment.
-.. _requires: http://tools.ietf.org/html/rfc6749#section-5.1
-.. _robustness principle: http://en.wikipedia.org/wiki/Robustness_principle
+.. _requires: https://tools.ietf.org/html/rfc6749#section-5.1
+.. _robustness principle: https://en.wikipedia.org/wiki/Robustness_principle
.. toctree::
:maxdepth: 2
diff --git a/docs/release_process.rst b/docs/release_process.rst
index aab97c4..9ee987c 100644
--- a/docs/release_process.rst
+++ b/docs/release_process.rst
@@ -2,12 +2,12 @@ Release process
===============
OAuthLib has got to a point where quite a few libraries and users depend on it.
-Because of this a more careful release procedure will be introduced to make
+Because of this, a more careful release procedure will be introduced to make
sure all these lovely projects don't suddenly break.
When approaching a release we will run the unittests for a set of downstream
libraries using the unreleased version of OAuthLib. If OAuthLib is the cause of
-failing tests we will either
+failing tests we will either:
1. Find a way to introduce the change without breaking downstream. However,
this is not always the best long term option.
@@ -25,7 +25,7 @@ OAuthLib release issue on Github at least 2 days prior to release detailing the
changes and pings the primary contacts for each downstream project. Please
respond within those 2 days if you have major concerns.
-How to get on the notifcations list
+How to get on the notifications list
-----------------------------------
Which projects and the instructions for testing each will be defined in
@@ -45,8 +45,8 @@ A note on versioning
--------------------
Historically OAuthLib has not been very good at semantic versioning but that
-will change after the 1.0.0 release due late 2014. After that poing any major
-digit release (e.g. 2.0.0) may introduce non backwards compatible changes.
+has changed since the 1.0.0 in 2014. Since, any major digit release
+(e.g. 2.0.0) may introduce non backwards compatible changes.
Minor point (1.1.0) releases will introduce non API breaking new features and
changes. Bug releases (1.0.1) will include minor fixes that needs to be
released quickly (e.g. after a bigger release unintentionally introduced a
diff --git a/examples/skeleton_oauth2_web_application_server.py b/examples/skeleton_oauth2_web_application_server.py
index 8bfd936..e53232f 100644
--- a/examples/skeleton_oauth2_web_application_server.py
+++ b/examples/skeleton_oauth2_web_application_server.py
@@ -67,7 +67,7 @@ class SkeletonValidator(RequestValidator):
# state and user to request.scopes and request.user.
pass
- def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs):
+ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request, *args, **kwargs):
# You did save the redirect uri with the authorization code right?
pass
diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py
index 459f307..b23102c 100644
--- a/oauthlib/__init__.py
+++ b/oauthlib/__init__.py
@@ -5,21 +5,13 @@
A generic, spec-compliant, thorough implementation of the OAuth
request-signing logic.
- :copyright: (c) 2011 by Idan Gazit.
+ :copyright: (c) 2019 by The OAuthlib Community
:license: BSD, see LICENSE for details.
"""
-
-__author__ = 'Idan Gazit <idan@gazit.me>'
-__version__ = '2.0.5'
-
-
import logging
-try: # Python 2.7+
- from logging import NullHandler
-except ImportError:
- class NullHandler(logging.Handler):
+from logging import NullHandler
- def emit(self, record):
- pass
+__author__ = 'The OAuthlib Community'
+__version__ = '3.0.1'
logging.getLogger('oauthlib').addHandler(NullHandler())
diff --git a/oauthlib/common.py b/oauthlib/common.py
index 705cbd2..970d7a5 100644
--- a/oauthlib/common.py
+++ b/oauthlib/common.py
@@ -11,12 +11,17 @@ from __future__ import absolute_import, unicode_literals
import collections
import datetime
import logging
-import random
import re
import sys
import time
try:
+ from secrets import randbits
+ from secrets import SystemRandom
+except ImportError:
+ from random import getrandbits as randbits
+ from random import SystemRandom
+try:
from urllib import quote as _quote
from urllib import unquote as _unquote
from urllib import urlencode as _urlencode
@@ -49,10 +54,8 @@ PY3 = sys.version_info[0] == 3
if PY3:
unicode_type = str
- bytes_type = bytes
else:
unicode_type = unicode
- bytes_type = str
# 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
@@ -61,7 +64,7 @@ def quote(s, safe=b'/'):
s = _quote(s, safe)
# PY3 always returns unicode. PY2 may return either, depending on whether
# it had to modify the string.
- if isinstance(s, bytes_type):
+ if isinstance(s, bytes):
s = s.decode('utf-8')
return s
@@ -71,7 +74,7 @@ def unquote(s):
# PY3 always returns unicode. PY2 seems to always return what you give it,
# which differs from quote's behavior. Just to be safe, make sure it is
# unicode before we return.
- if isinstance(s, bytes_type):
+ if isinstance(s, bytes):
s = s.decode('utf-8')
return s
@@ -104,12 +107,12 @@ def decode_params_utf8(params):
decoded = []
for k, v in params:
decoded.append((
- k.decode('utf-8') if isinstance(k, bytes_type) else k,
- v.decode('utf-8') if isinstance(v, bytes_type) else v))
+ k.decode('utf-8') if isinstance(k, bytes) else k,
+ v.decode('utf-8') if isinstance(v, bytes) else v))
return decoded
-urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?')
+urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?\'$')
def urldecode(query):
@@ -169,7 +172,7 @@ def extract_params(raw):
empty list of parameters. Any other input will result in a return
value of None.
"""
- if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
+ if isinstance(raw, bytes) or isinstance(raw, unicode_type):
try:
params = urldecode(raw)
except ValueError:
@@ -199,10 +202,10 @@ def generate_nonce():
A random 64-bit number is appended to the epoch timestamp for both
randomness and to decrease the likelihood of collisions.
- .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
- .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+ .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
"""
- return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
+ return unicode_type(unicode_type(randbits(64)) + generate_timestamp())
def generate_timestamp():
@@ -211,8 +214,8 @@ def generate_timestamp():
Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
Per `section 3.2.1`_ of the MAC Access Authentication spec.
- .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
- .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+ .. _`section 3.2.1`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+ .. _`section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
"""
return unicode_type(int(time.time()))
@@ -225,7 +228,7 @@ def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
and entropy when generating the random characters is important. Which is
why SystemRandom is used instead of the default random.choice method.
"""
- rand = random.SystemRandom()
+ rand = SystemRandom()
return ''.join(rand.choice(chars) for x in range(length))
@@ -257,7 +260,7 @@ def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
"""Generates an OAuth client_id
OAuth 2 specify the format of client_id in
- http://tools.ietf.org/html/rfc6749#appendix-A.
+ https://tools.ietf.org/html/rfc6749#appendix-A.
"""
return generate_token(length, chars)
@@ -304,7 +307,7 @@ def to_unicode(data, encoding='UTF-8'):
if isinstance(data, unicode_type):
return data
- if isinstance(data, bytes_type):
+ if isinstance(data, bytes):
return unicode_type(data, encoding=encoding)
if hasattr(data, '__iter__'):
@@ -394,6 +397,9 @@ class Request(object):
"client_id": None,
"client_secret": None,
"code": None,
+ "code_challenge": None,
+ "code_challenge_method": None,
+ "code_verifier": None,
"extra_credentials": None,
"grant_type": None,
"redirect_uri": None,
@@ -421,7 +427,6 @@ class Request(object):
}
self._params.update(dict(urldecode(self.uri_query)))
self._params.update(dict(self.decoded_body or []))
- self._params.update(self.headers)
def __getattr__(self, name):
if name in self._params:
diff --git a/oauthlib/oauth1/__init__.py b/oauthlib/oauth1/__init__.py
index f9dff74..dc908d4 100644
--- a/oauthlib/oauth1/__init__.py
+++ b/oauthlib/oauth1/__init__.py
@@ -9,7 +9,7 @@ and Server classes.
from __future__ import absolute_import, unicode_literals
from .rfc5849 import Client
-from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT
+from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT
from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY
from .rfc5849 import SIGNATURE_TYPE_BODY
from .rfc5849.request_validator import RequestValidator
diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py
index 06902e2..7313286 100644
--- a/oauthlib/oauth1/rfc5849/__init__.py
+++ b/oauthlib/oauth1/rfc5849/__init__.py
@@ -18,19 +18,16 @@ try:
except ImportError:
import urllib.parse as urlparse
-if sys.version_info[0] == 3:
- bytes_type = bytes
-else:
- bytes_type = str
-
from oauthlib.common import Request, urlencode, generate_nonce
from oauthlib.common import generate_timestamp, to_unicode
from . import parameters, signature
-SIGNATURE_HMAC = "HMAC-SHA1"
+SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
+SIGNATURE_HMAC_SHA256 = "HMAC-SHA256"
+SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1
SIGNATURE_RSA = "RSA-SHA1"
SIGNATURE_PLAINTEXT = "PLAINTEXT"
-SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
+SIGNATURE_METHODS = (SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
SIGNATURE_TYPE_QUERY = 'QUERY'
@@ -43,7 +40,8 @@ class Client(object):
"""A client used to sign OAuth 1.0 RFC 5849 requests."""
SIGNATURE_METHODS = {
- SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client,
+ SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client,
+ SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client,
SIGNATURE_RSA: signature.sign_rsa_sha1_with_client,
SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
}
@@ -57,7 +55,7 @@ class Client(object):
resource_owner_key=None,
resource_owner_secret=None,
callback_uri=None,
- signature_method=SIGNATURE_HMAC,
+ signature_method=SIGNATURE_HMAC_SHA1,
signature_type=SIGNATURE_TYPE_AUTH_HEADER,
rsa_key=None, verifier=None, realm=None,
encoding='utf-8', decoding=None,
@@ -119,7 +117,7 @@ class Client(object):
replace any netloc part of the request argument's uri attribute
value.
- .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
"""
if self.signature_method == SIGNATURE_PLAINTEXT:
# fast-path
@@ -175,10 +173,12 @@ class Client(object):
params.append(('oauth_verifier', self.verifier))
# providing body hash for requests other than x-www-form-urlencoded
- # as described in http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+ # as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1
# 4.1.1. When to include the body hash
# * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
# * [...] SHOULD include the oauth_body_hash parameter on all other requests.
+ # Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2
+ # At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension.
content_type = request.headers.get('Content-Type', None)
content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
if request.body is not None and content_type_eligible:
@@ -297,7 +297,7 @@ class Client(object):
raise ValueError(
'Body signatures may only be used with form-urlencoded content')
- # We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ # We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
# with the clause that parameters from body should only be included
# in non GET or HEAD requests. Extracting the request body parameters
# and including them in the signature base string would give semantic
diff --git a/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/oauthlib/oauth1/rfc5849/endpoints/access_token.py
index 12b901c..bea8274 100644
--- a/oauthlib/oauth1/rfc5849/endpoints/access_token.py
+++ b/oauthlib/oauth1/rfc5849/endpoints/access_token.py
@@ -37,7 +37,8 @@ class AccessTokenEndpoint(BaseEndpoint):
Similar to OAuth 2, indication of granted scopes will be included as a
space separated list in ``oauth_authorized_realms``.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The token as an urlencoded string.
"""
request.realms = self.request_validator.get_realms(
@@ -120,7 +121,8 @@ class AccessTokenEndpoint(BaseEndpoint):
def validate_access_token_request(self, request):
"""Validate an access token request.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:raises: OAuth1Error if the request is invalid.
:returns: A tuple of 2 elements.
1. The validation result (True or False).
@@ -180,7 +182,7 @@ class AccessTokenEndpoint(BaseEndpoint):
# token credentials to the client, and ensure that the temporary
# credentials have not expired or been used before. The server MUST
# also verify the verification code received from the client.
- # .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
+ # .. _`Section 3.2`: https://tools.ietf.org/html/rfc5849#section-3.2
#
# Note that early exit would enable resource owner authorization
# verifier enumertion.
diff --git a/oauthlib/oauth1/rfc5849/endpoints/authorization.py b/oauthlib/oauth1/rfc5849/endpoints/authorization.py
index 1751a45..b465946 100644
--- a/oauthlib/oauth1/rfc5849/endpoints/authorization.py
+++ b/oauthlib/oauth1/rfc5849/endpoints/authorization.py
@@ -42,7 +42,8 @@ class AuthorizationEndpoint(BaseEndpoint):
def create_verifier(self, request, credentials):
"""Create and save a new request token.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:param credentials: A dict of extra token credentials.
:returns: The verifier as a dict.
"""
diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py
index 9d51e69..9702939 100644
--- a/oauthlib/oauth1/rfc5849/endpoints/base.py
+++ b/oauthlib/oauth1/rfc5849/endpoints/base.py
@@ -127,7 +127,7 @@ class BaseEndpoint(object):
# specification. Implementers should review the Security
# Considerations section (`Section 4`_) before deciding on which
# method to support.
- # .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4
+ # .. _`Section 4`: https://tools.ietf.org/html/rfc5849#section-4
if (not request.signature_method in
self.request_validator.allowed_signature_methods):
raise errors.InvalidSignatureMethodError(
@@ -181,7 +181,7 @@ class BaseEndpoint(object):
# ---- RSA Signature verification ----
if request.signature_method == SIGNATURE_RSA:
# The server verifies the signature per `[RFC3447] section 8.2.2`_
- # .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1
+ # .. _`[RFC3447] section 8.2.2`: https://tools.ietf.org/html/rfc3447#section-8.2.1
rsa_key = self.request_validator.get_rsa_key(
request.client_key, request)
valid_signature = signature.verify_rsa_sha1(request, rsa_key)
@@ -192,7 +192,7 @@ class BaseEndpoint(object):
# Recalculating the request signature independently as described in
# `Section 3.4`_ and comparing it to the value received from the
# client via the "oauth_signature" parameter.
- # .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+ # .. _`Section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
client_secret = self.request_validator.get_client_secret(
request.client_key, request)
resource_owner_secret = None
diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py
index 515395b..e9ca331 100644
--- a/oauthlib/oauth1/rfc5849/endpoints/request_token.py
+++ b/oauthlib/oauth1/rfc5849/endpoints/request_token.py
@@ -34,7 +34,8 @@ class RequestTokenEndpoint(BaseEndpoint):
def create_request_token(self, request, credentials):
"""Create and save a new request token.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:param credentials: A dict of extra token credentials.
:returns: The token as an urlencoded string.
"""
@@ -111,7 +112,8 @@ class RequestTokenEndpoint(BaseEndpoint):
def validate_request_token_request(self, request):
"""Validate a request token request.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:raises: OAuth1Error if the request is invalid.
:returns: A tuple of 2 elements.
1. The validation result (True or False).
@@ -156,7 +158,7 @@ class RequestTokenEndpoint(BaseEndpoint):
# However they could be seen as a scope or realm to which the
# client has access and as such every client should be checked
# to ensure it is authorized access to that scope or realm.
- # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
+ # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2
#
# Note that early exit would enable client realm access enumeration.
#
@@ -178,7 +180,7 @@ class RequestTokenEndpoint(BaseEndpoint):
# Callback is normally never required, except for requests for
# a Temporary Credential as described in `Section 2.1`_
- # .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1
+ # .._`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
valid_redirect = self.request_validator.validate_redirect_uri(
request.client_key, request.redirect_uri, request)
if not request.redirect_uri:
diff --git a/oauthlib/oauth1/rfc5849/endpoints/resource.py b/oauthlib/oauth1/rfc5849/endpoints/resource.py
index 53f9562..f82e8b1 100644
--- a/oauthlib/oauth1/rfc5849/endpoints/resource.py
+++ b/oauthlib/oauth1/rfc5849/endpoints/resource.py
@@ -119,7 +119,7 @@ class ResourceEndpoint(BaseEndpoint):
# However they could be seen as a scope or realm to which the
# client has access and as such every client should be checked
# to ensure it is authorized access to that scope or realm.
- # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
+ # .. _`realm`: https://tools.ietf.org/html/rfc2617#section-1.2
#
# Note that early exit would enable client realm access enumeration.
#
diff --git a/oauthlib/oauth1/rfc5849/parameters.py b/oauthlib/oauth1/rfc5849/parameters.py
index dcb23dc..db4400e 100644
--- a/oauthlib/oauth1/rfc5849/parameters.py
+++ b/oauthlib/oauth1/rfc5849/parameters.py
@@ -5,7 +5,7 @@ oauthlib.parameters
This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
-.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5
+.. _`section 3.5`: https://tools.ietf.org/html/rfc5849#section-3.5
"""
from __future__ import absolute_import, unicode_literals
@@ -15,7 +15,7 @@ from . import utils
try:
from urlparse import urlparse, urlunparse
-except ImportError:
+except ImportError: # noqa
from urllib.parse import urlparse, urlunparse
@@ -42,8 +42,8 @@ def prepare_headers(oauth_params, headers=None, realm=None):
oauth_version="1.0"
- .. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
- .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+ .. _`section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1
+ .. _`RFC2617`: https://tools.ietf.org/html/rfc2617
"""
headers = headers or {}
@@ -54,7 +54,7 @@ def prepare_headers(oauth_params, headers=None, realm=None):
# 1. Parameter names and values are encoded per Parameter Encoding
# (`Section 3.6`_)
#
- # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
escaped_name = utils.escape(oauth_parameter_name)
escaped_value = utils.escape(value)
@@ -68,14 +68,14 @@ def prepare_headers(oauth_params, headers=None, realm=None):
# 3. Parameters are separated by a "," character (ASCII code 44) and
# OPTIONAL linear whitespace per `RFC2617`_.
#
- # .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+ # .. _`RFC2617`: https://tools.ietf.org/html/rfc2617
authorization_header_parameters = ', '.join(
authorization_header_parameters_parts)
# 4. The OPTIONAL "realm" parameter MAY be added and interpreted per
# `RFC2617 section 1.2`_.
#
- # .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2
+ # .. _`RFC2617 section 1.2`: https://tools.ietf.org/html/rfc2617#section-1.2
if realm:
# NOTE: realm should *not* be escaped
authorization_header_parameters = ('realm="%s", ' % realm +
@@ -98,8 +98,8 @@ def _append_params(oauth_params, params):
Per `section 3.5.2`_ and `3.5.3`_ of the spec.
- .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
- .. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+ .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2
+ .. _`3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3
"""
merged = list(params)
@@ -117,7 +117,7 @@ def prepare_form_encoded_body(oauth_params, body):
Per `section 3.5.2`_ of the spec.
- .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
+ .. _`section 3.5.2`: https://tools.ietf.org/html/rfc5849#section-3.5.2
"""
# append OAuth params to the existing body
@@ -129,7 +129,7 @@ def prepare_request_uri_query(oauth_params, uri):
Per `section 3.5.3`_ of the spec.
- .. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+ .. _`section 3.5.3`: https://tools.ietf.org/html/rfc5849#section-3.5.3
"""
# append OAuth params to the existing set of query components
diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py
index 2ccb367..330bcbb 100644
--- a/oauthlib/oauth1/rfc5849/request_validator.py
+++ b/oauthlib/oauth1/rfc5849/request_validator.py
@@ -109,7 +109,7 @@ class RequestValidator(object):
their use more straightforward and as such it could be worth reading what
follows in chronological order.
- .. _`whitelisting or blacklisting`: http://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
+ .. _`whitelisting or blacklisting`: https://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
"""
def __init__(self):
@@ -267,7 +267,8 @@ class RequestValidator(object):
"""Retrieves the client secret associated with the client key.
:param client_key: The client/consumer key.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The client secret as a string.
This method must allow the use of a dummy client_key value.
@@ -303,7 +304,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: The request token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The token secret as a string.
This method must allow the use of a dummy values and the running time
@@ -335,7 +337,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: The access token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The token secret as a string.
This method must allow the use of a dummy values and the running time
@@ -366,7 +369,8 @@ class RequestValidator(object):
"""Get the default realms for a client.
:param client_key: The client/consumer key.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The list of default realms associated with the client.
The list of default realms will be set during client registration and
@@ -382,7 +386,8 @@ class RequestValidator(object):
"""Get realms associated with a request token.
:param token: The request token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The list of realms associated with the request token.
This method is used by
@@ -396,7 +401,8 @@ class RequestValidator(object):
"""Get the redirect URI associated with a request token.
:param token: The request token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The redirect URI associated with the request token.
It may be desirable to return a custom URI if the redirect is set to "oob".
@@ -413,7 +419,8 @@ class RequestValidator(object):
"""Retrieves a previously stored client provided RSA key.
:param client_key: The client/consumer key.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: The rsa public key as a string.
This method must allow the use of a dummy client_key value. Fetching
@@ -437,7 +444,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param request_token: The request token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: None
Per `Section 2.3`__ of the spec:
@@ -445,7 +453,7 @@ class RequestValidator(object):
"The server MUST (...) ensure that the temporary
credentials have not expired or been used before."
- .. _`Section 2.3`: http://tools.ietf.org/html/rfc5849#section-2.3
+ .. _`Section 2.3`: https://tools.ietf.org/html/rfc5849#section-2.3
This method should ensure that provided token won't validate anymore.
It can be simply removing RequestToken from storage or setting
@@ -462,7 +470,8 @@ class RequestValidator(object):
"""Validates that supplied client key is a registered and valid client.
:param client_key: The client/consumer key.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
Note that if the dummy client is supplied it should validate in same
@@ -499,7 +508,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: The request token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
Note that if the dummy request_token is supplied it should validate in
@@ -533,7 +543,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: The access token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
Note that if the dummy access token is supplied it should validate in
@@ -571,7 +582,8 @@ class RequestValidator(object):
:param nonce: The ``oauth_nonce`` parameter.
:param request_token: Request token string, if any.
:param access_token: Access token string, if any.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
Per `Section 3.3`_ of the spec.
@@ -582,7 +594,7 @@ class RequestValidator(object):
channel. The nonce value MUST be unique across all requests with the
same timestamp, client credentials, and token combinations."
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3
One of the first validation checks that will be made is for the validity
of the nonce and timestamp, which are associated with a client key and
@@ -618,7 +630,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param redirect_uri: The URI the client which to redirect back to after
authorization is successful.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
It is highly recommended that OAuth providers require their clients
@@ -650,7 +663,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param realms: The list of realms that client is requesting access to.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
This method is invoked when obtaining a request token and should
@@ -669,7 +683,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: A request token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:param uri: The URI the realms is protecting.
:param realms: A list of realms that must have been granted to
the access token.
@@ -703,7 +718,8 @@ class RequestValidator(object):
:param client_key: The client/consumer key.
:param token: A request token string.
:param verifier: The authorization verifier string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
OAuth providers issue a verification code to clients after the
@@ -732,7 +748,8 @@ class RequestValidator(object):
"""Verify that the given OAuth1 request token is valid.
:param token: A request token string.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
This method is used only in AuthorizationEndpoint to check whether the
@@ -751,7 +768,8 @@ class RequestValidator(object):
:param token: An access token string.
:param realms: A list of realms the client attempts to access.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:returns: True or False
This prevents the list of authorized realms sent by the client during
@@ -773,7 +791,8 @@ class RequestValidator(object):
"""Save an OAuth1 access token.
:param token: A dict with token credentials.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
The token dictionary will at minimum include
@@ -796,7 +815,8 @@ class RequestValidator(object):
"""Save an OAuth1 request token.
:param token: A dict with token credentials.
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
The token dictionary will at minimum include
@@ -818,7 +838,8 @@ class RequestValidator(object):
:param token: A request token string.
:param verifier A dictionary containing the oauth_verifier and
oauth_token
- :param request: An oauthlib.common.Request object.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
We need to associate verifiers with tokens for validation during the
access token request.
diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py
index 10d057f..e90d6f3 100644
--- a/oauthlib/oauth1/rfc5849/signature.py
+++ b/oauthlib/oauth1/rfc5849/signature.py
@@ -19,7 +19,7 @@ Steps for signing a request:
construct the base string
5. Pass the base string and any keys needed to a signing function
-.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
"""
from __future__ import absolute_import, unicode_literals
@@ -28,7 +28,7 @@ import hashlib
import hmac
import logging
-from oauthlib.common import (bytes_type, extract_params, safe_string_equals,
+from oauthlib.common import (extract_params, safe_string_equals,
unicode_type, urldecode)
from . import utils
@@ -69,7 +69,7 @@ def construct_base_string(http_method, base_string_uri,
ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
9d7dh3k39sjv7
- .. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ .. _`section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
"""
# The signature base string is constructed by concatenating together,
@@ -79,7 +79,7 @@ def construct_base_string(http_method, base_string_uri,
# "GET", "POST", etc. If the request uses a custom HTTP method, it
# MUST be encoded (`Section 3.6`_).
#
- # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
base_string = utils.escape(http_method.upper())
# 2. An "&" character (ASCII code 38).
@@ -88,8 +88,8 @@ def construct_base_string(http_method, base_string_uri,
# 3. The base string URI from `Section 3.4.1.2`_, after being encoded
# (`Section 3.6`_).
#
- # .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
- # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+ # .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ # .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6
base_string += utils.escape(base_string_uri)
# 4. An "&" character (ASCII code 38).
@@ -98,8 +98,8 @@ def construct_base_string(http_method, base_string_uri,
# 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after
# being encoded (`Section 3.6`).
#
- # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
- # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+ # .. _`Section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ # .. _`Section 3.4.6`: https://tools.ietf.org/html/rfc5849#section-3.4.6
base_string += utils.escape(normalized_encoded_request_parameters)
return base_string
@@ -123,7 +123,7 @@ def normalize_base_string_uri(uri, host=None):
is represented by the base string URI: "https://www.example.net:8080/".
- .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+ .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
The host argument overrides the netloc part of the uri argument.
"""
@@ -137,7 +137,7 @@ def normalize_base_string_uri(uri, host=None):
# are included by constructing an "http" or "https" URI representing
# the request resource (without the query or fragment) as follows:
#
- # .. _`RFC3986`: http://tools.ietf.org/html/rfc3986
+ # .. _`RFC3986`: https://tools.ietf.org/html/rfc3986
if not scheme or not netloc:
raise ValueError('uri must include a scheme and netloc')
@@ -147,7 +147,7 @@ def normalize_base_string_uri(uri, host=None):
# Note that the absolute path cannot be empty; if none is present in
# the original URI, it MUST be given as "/" (the server root).
#
- # .. _`RFC 2616 section 5.1.2`: http://tools.ietf.org/html/rfc2616#section-5.1.2
+ # .. _`RFC 2616 section 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2
if not path:
path = '/'
@@ -166,8 +166,8 @@ def normalize_base_string_uri(uri, host=None):
# to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
# All other non-default port numbers MUST be included.
#
- # .. _`RFC2616`: http://tools.ietf.org/html/rfc2616
- # .. _`RFC2818`: http://tools.ietf.org/html/rfc2818
+ # .. _`RFC2616`: https://tools.ietf.org/html/rfc2616
+ # .. _`RFC2818`: https://tools.ietf.org/html/rfc2818
default_ports = (
('http', '80'),
('https', '443'),
@@ -190,7 +190,7 @@ def normalize_base_string_uri(uri, host=None):
# particular manner that is often different from their original
# encoding scheme, and concatenated into a single string.
#
-# .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+# .. _`section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3
def collect_parameters(uri_query='', body=[], headers=None,
exclude_oauth_signature=True, with_realm=False):
@@ -249,7 +249,7 @@ def collect_parameters(uri_query='', body=[], headers=None,
parameter instances (the "a3" parameter is used twice in this
request).
- .. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+ .. _`section 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
"""
headers = headers or {}
params = []
@@ -264,8 +264,8 @@ def collect_parameters(uri_query='', body=[], headers=None,
# and values and decoding them as defined by
# `W3C.REC-html40-19980424`_, Section 17.13.4.
#
- # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4
- # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+ # .. _`RFC3986, Section 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4
+ # .. _`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
if uri_query:
params.extend(urldecode(uri_query))
@@ -274,7 +274,7 @@ def collect_parameters(uri_query='', body=[], headers=None,
# pairs excluding the "realm" parameter if present. The parameter
# values are decoded as defined by `Section 3.5.1`_.
#
- # .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
+ # .. _`Section 3.5.1`: https://tools.ietf.org/html/rfc5849#section-3.5.1
if headers:
headers_lower = dict((k.lower(), v) for k, v in headers.items())
authorization_header = headers_lower.get('authorization')
@@ -293,7 +293,7 @@ def collect_parameters(uri_query='', body=[], headers=None,
# * The HTTP request entity-header includes the "Content-Type"
# header field set to "application/x-www-form-urlencoded".
#
- # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+ # .._`W3C.REC-html40-19980424`: https://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
# TODO: enforce header param inclusion conditions
bodyparams = extract_params(body) or []
@@ -383,18 +383,18 @@ def normalize_parameters(params):
dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
- .. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ .. _`section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
"""
# The parameters collected in `Section 3.4.1.3`_ are normalized into a
# single string as follows:
#
- # .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+ # .. _`Section 3.4.1.3`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3
# 1. First, the name and value of each parameter are encoded
# (`Section 3.6`_).
#
- # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
# 2. The parameters are sorted by name, using ascending byte value
@@ -430,8 +430,8 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
Per `section 3.4.2`_ of the spec.
- .. _`RFC2104`: http://tools.ietf.org/html/rfc2104
- .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2
+ .. _`RFC2104`: https://tools.ietf.org/html/rfc2104
+ .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
"""
# The HMAC-SHA1 function variables are used in following way:
@@ -439,13 +439,13 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
# text is set to the value of the signature base string from
# `Section 3.4.1.1`_.
#
- # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
text = base_string
# key is set to the concatenated values of:
# 1. The client shared-secret, after being encoded (`Section 3.6`_).
#
- # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
key = utils.escape(client_secret or '')
# 2. An "&" character (ASCII code 38), which MUST be included
@@ -454,7 +454,7 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
# 3. The token shared-secret, after being encoded (`Section 3.6`_).
#
- # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
key += utils.escape(resource_owner_secret or '')
# FIXME: HMAC does not support unicode!
@@ -466,7 +466,64 @@ def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
# parameter, after the result octet string is base64-encoded
# per `RFC2045, Section 6.8`.
#
- # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8
+ # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
+ return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+
+def sign_hmac_sha256_with_client(base_string, client):
+ return sign_hmac_sha256(base_string,
+ client.client_secret,
+ client.resource_owner_secret
+ )
+
+
+def sign_hmac_sha256(base_string, client_secret, resource_owner_secret):
+ """**HMAC-SHA256**
+
+ The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature
+ algorithm as defined in `RFC4634`_::
+
+ digest = HMAC-SHA256 (key, text)
+
+ Per `section 3.4.2`_ of the spec.
+
+ .. _`RFC4634`: https://tools.ietf.org/html/rfc4634
+ .. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
+ """
+
+ # The HMAC-SHA256 function variables are used in following way:
+
+ # text is set to the value of the signature base string from
+ # `Section 3.4.1.1`_.
+ #
+ # .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
+ text = base_string
+
+ # key is set to the concatenated values of:
+ # 1. The client shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ key = utils.escape(client_secret or '')
+
+ # 2. An "&" character (ASCII code 38), which MUST be included
+ # even when either secret is empty.
+ key += '&'
+
+ # 3. The token shared-secret, after being encoded (`Section 3.6`_).
+ #
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
+ key += utils.escape(resource_owner_secret or '')
+
+ # FIXME: HMAC does not support unicode!
+ key_utf8 = key.encode('utf-8')
+ text_utf8 = text.encode('utf-8')
+ signature = hmac.new(key_utf8, text_utf8, hashlib.sha256)
+
+ # digest is used to set the value of the "oauth_signature" protocol
+ # parameter, after the result octet string is base64-encoded
+ # per `RFC2045, Section 6.8`.
+ #
+ # .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
_jwtrs1 = None
@@ -491,8 +548,8 @@ def sign_rsa_sha1(base_string, rsa_private_key):
with the server that included its RSA public key (in a manner that is
beyond the scope of this specification).
- .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
- .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2
+ .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
+ .. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2
"""
if isinstance(base_string, unicode_type):
@@ -521,7 +578,7 @@ def sign_plaintext(client_secret, resource_owner_secret):
utilize the signature base string or the "oauth_timestamp" and
"oauth_nonce" parameters.
- .. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4
+ .. _`section 3.4.4`: https://tools.ietf.org/html/rfc5849#section-3.4.4
"""
@@ -530,7 +587,7 @@ def sign_plaintext(client_secret, resource_owner_secret):
# 1. The client shared-secret, after being encoded (`Section 3.6`_).
#
- # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
signature = utils.escape(client_secret or '')
# 2. An "&" character (ASCII code 38), which MUST be included even
@@ -539,7 +596,7 @@ def sign_plaintext(client_secret, resource_owner_secret):
# 3. The token shared-secret, after being encoded (`Section 3.6`_).
#
- # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ # .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
signature += utils.escape(resource_owner_secret or '')
return signature
@@ -555,7 +612,7 @@ def verify_hmac_sha1(request, client_secret=None,
Per `section 3.4`_ of the spec.
- .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+ .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
attribute MUST be an absolute URI whose netloc part identifies the
@@ -563,7 +620,7 @@ def verify_hmac_sha1(request, client_secret=None,
item of the request argument's headers dict attribute will be
ignored.
- .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
+ .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
"""
norm_params = normalize_parameters(request.params)
@@ -578,7 +635,7 @@ def verify_hmac_sha1(request, client_secret=None,
def _prepare_key_plus(alg, keystr):
- if isinstance(keystr, bytes_type):
+ if isinstance(keystr, bytes):
keystr = keystr.decode('utf-8')
return alg.prepare_key(keystr)
@@ -589,7 +646,7 @@ def verify_rsa_sha1(request, rsa_public_key):
Note this method requires the jwt and cryptography libraries.
- .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
+ .. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
attribute MUST be an absolute URI whose netloc part identifies the
@@ -597,7 +654,7 @@ def verify_rsa_sha1(request, rsa_public_key):
item of the request argument's headers dict attribute will be
ignored.
- .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
+ .. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
"""
norm_params = normalize_parameters(request.params)
uri = normalize_base_string_uri(request.uri)
@@ -618,7 +675,7 @@ def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
Per `section 3.4`_ of the spec.
- .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+ .. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
"""
signature = sign_plaintext(client_secret, resource_owner_secret)
match = safe_string_equals(signature, request.signature)
diff --git a/oauthlib/oauth1/rfc5849/utils.py b/oauthlib/oauth1/rfc5849/utils.py
index 979e5f6..735f21d 100644
--- a/oauthlib/oauth1/rfc5849/utils.py
+++ b/oauthlib/oauth1/rfc5849/utils.py
@@ -8,7 +8,7 @@ spec.
"""
from __future__ import absolute_import, unicode_literals
-from oauthlib.common import bytes_type, quote, unicode_type, unquote
+from oauthlib.common import quote, unicode_type, unquote
try:
import urllib2
@@ -49,7 +49,7 @@ def escape(u):
Per `section 3.6`_ of the spec.
- .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+ .. _`section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
"""
if not isinstance(u, unicode_type):
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py
index c8d934e..3f43755 100644
--- a/oauthlib/oauth2/__init__.py
+++ b/oauthlib/oauth2/__init__.py
@@ -15,6 +15,8 @@ from .rfc6749.clients import LegacyApplicationClient
from .rfc6749.clients import BackendApplicationClient
from .rfc6749.clients import ServiceApplicationClient
from .rfc6749.endpoints import AuthorizationEndpoint
+from .rfc6749.endpoints import IntrospectEndpoint
+from .rfc6749.endpoints import MetadataEndpoint
from .rfc6749.endpoints import TokenEndpoint
from .rfc6749.endpoints import ResourceEndpoint
from .rfc6749.endpoints import RevocationEndpoint
@@ -23,7 +25,7 @@ from .rfc6749.endpoints import WebApplicationServer
from .rfc6749.endpoints import MobileApplicationServer
from .rfc6749.endpoints import LegacyApplicationServer
from .rfc6749.endpoints import BackendApplicationServer
-from .rfc6749.errors import AccessDeniedError, AccountSelectionRequired, ConsentRequired, FatalClientError, FatalOpenIDClientError, InsecureTransportError, InteractionRequired, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, LoginRequired, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, OAuth2Error, OpenIDClientError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError
+from .rfc6749.errors import AccessDeniedError, OAuth2Error, FatalClientError, InsecureTransportError, InvalidClientError, InvalidClientIdError, InvalidGrantError, InvalidRedirectURIError, InvalidRequestError, InvalidRequestFatalError, InvalidScopeError, MismatchingRedirectURIError, MismatchingStateError, MissingClientIdError, MissingCodeError, MissingRedirectURIError, MissingResponseTypeError, MissingTokenError, MissingTokenTypeError, ServerError, TemporarilyUnavailableError, TokenExpiredError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError
from .rfc6749.grant_types import AuthorizationCodeGrant
from .rfc6749.grant_types import ImplicitGrant
from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant
diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py
index 7505b0d..a000ecf 100644
--- a/oauthlib/oauth2/rfc6749/clients/backend_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py
@@ -29,16 +29,29 @@ class BackendApplicationClient(Client):
Since the client authentication is used as the authorization grant,
no additional authorization request is needed.
"""
-
- def prepare_request_body(self, body='', scope=None, **kwargs):
+
+ grant_type = 'client_credentials'
+
+ def prepare_request_body(self, body='', scope=None,
+ include_client_id=None, **kwargs):
"""Add the client credentials to the request body.
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format per `Appendix B`_ in the HTTP request entity-body:
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
:param scope: The scope of the access request as described by
`Section 3.3`_.
+
+ :param include_client_id: `True` to send the `client_id` in the body of
+ the upstream request. Default `None`. This is
+ required if the client is not authenticating
+ with the authorization server as described
+ in `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
:param kwargs: Extra credentials to include in the token request.
The client MUST authenticate with the authorization server as
@@ -52,9 +65,11 @@ class BackendApplicationClient(Client):
>>> client.prepare_request_body(scope=['hello', 'world'])
'grant_type=client_credentials&scope=hello+world'
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
- return prepare_token_request('client_credentials', body=body,
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ return prepare_token_request(self.grant_type, body=body,
scope=scope, **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py
index c2f8809..1a50644 100644
--- a/oauthlib/oauth2/rfc6749/clients/base.py
+++ b/oauthlib/oauth2/rfc6749/clients/base.py
@@ -9,6 +9,7 @@ for consuming OAuth 2.0 RFC6749.
from __future__ import absolute_import, unicode_literals
import time
+import warnings
from oauthlib.common import generate_token
from oauthlib.oauth2.rfc6749 import tokens
@@ -46,6 +47,7 @@ class Client(object):
Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
"""
+ refresh_token_key = 'refresh_token'
def __init__(self, client_id,
default_token_placement=AUTH_HEADER,
@@ -111,8 +113,10 @@ class Client(object):
self.state_generator = state_generator
self.state = state
self.redirect_url = redirect_url
+ self.code = None
+ self.expires_in = None
self._expires_at = None
- self._populate_attributes(self.token)
+ self.populate_token_attributes(self.token)
@property
def token_types(self):
@@ -140,6 +144,7 @@ class Client(object):
def parse_request_uri_response(self, *args, **kwargs):
"""Abstract method used to parse redirection responses."""
+ raise NotImplementedError("Must be implemented by inheriting classes.")
def add_token(self, uri, http_method='GET', body=None, headers=None,
token_placement=None, **kwargs):
@@ -173,8 +178,8 @@ class Client(object):
nonce="274312:dj83hs9s",
mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
- .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2
- .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2
+ .. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2
+ .. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2
"""
if not is_secure_transport(uri):
raise InsecureTransportError()
@@ -186,7 +191,7 @@ class Client(object):
if not self.token_type.lower() in case_insensitive_token_types:
raise ValueError("Unsupported token type: %s" % self.token_type)
- if not self.access_token:
+ if not (self.access_token or self.token.get('access_token')):
raise ValueError("Missing access token.")
if self._expires_at and self._expires_at < time.time():
@@ -250,7 +255,8 @@ class Client(object):
:param redirect_url: The redirect_url supplied with the authorization
request (if there was one).
- :param body: Request body (URL encoded string).
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
:param kwargs: Additional parameters to included in the request.
@@ -282,7 +288,8 @@ class Client(object):
:param refresh_token: Refresh token string.
- :param body: Request body (URL encoded string).
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
:param scope: List of scopes to request. Must be equal to
or a subset of the scopes granted when obtaining the refresh
@@ -401,12 +408,12 @@ class Client(object):
Providers may supply this in all responses but are required to only
if it has changed since the authorization request.
- .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
- .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
- .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
"""
self.token = parse_token_response(body, scope=scope)
- self._populate_attributes(self.token)
+ self.populate_token_attributes(self.token)
return self.token
def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
@@ -429,7 +436,7 @@ class Client(object):
resource owner.
"""
refresh_token = refresh_token or self.refresh_token
- return prepare_token_request('refresh_token', body=body, scope=scope,
+ return prepare_token_request(self.refresh_token_key, body=body, scope=scope,
refresh_token=refresh_token, **kwargs)
def _add_bearer_token(self, uri, http_method='GET', body=None,
@@ -460,7 +467,18 @@ class Client(object):
return uri, headers, body
def _populate_attributes(self, response):
- """Add commonly used values such as access_token to self."""
+ warnings.warn("Please switch to the public method "
+ "populate_token_attributes.", DeprecationWarning)
+ return self.populate_token_attributes(response)
+
+ def populate_code_attributes(self, response):
+ """Add attributes from an auth code response to self."""
+
+ if 'code' in response:
+ self.code = response.get('code')
+
+ def populate_token_attributes(self, response):
+ """Add attributes from a token exchange response to self."""
if 'access_token' in response:
self.access_token = response.get('access_token')
@@ -478,9 +496,6 @@ class Client(object):
if 'expires_at' in response:
self._expires_at = int(response.get('expires_at'))
- if 'code' in response:
- self.code = response.get('code')
-
if 'mac_key' in response:
self.mac_key = response.get('mac_key')
diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py
index 57fe99e..2449363 100644
--- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py
@@ -34,11 +34,14 @@ class LegacyApplicationClient(Client):
credentials is beyond the scope of this specification. The client
MUST discard the credentials once an access token has been obtained.
"""
+
+ grant_type = 'password'
def __init__(self, client_id, **kwargs):
super(LegacyApplicationClient, self).__init__(client_id, **kwargs)
- def prepare_request_body(self, username, password, body='', scope=None, **kwargs):
+ def prepare_request_body(self, username, password, body='', scope=None,
+ include_client_id=None, **kwargs):
"""Add the resource owner password and username to the request body.
The client makes a request to the token endpoint by adding the
@@ -47,8 +50,16 @@ class LegacyApplicationClient(Client):
:param username: The resource owner username.
:param password: The resource owner password.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
:param scope: The scope of the access request as described by
`Section 3.3`_.
+ :param include_client_id: `True` to send the `client_id` in the body of
+ the upstream request. Default `None`. This is
+ required if the client is not authenticating
+ with the authorization server as described
+ in `Section 3.2.1`_.
+ :type include_client_id: Boolean
:param kwargs: Extra credentials to include in the token request.
If the client type is confidential or the client was issued client
@@ -64,9 +75,11 @@ class LegacyApplicationClient(Client):
>>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world'])
'grant_type=password&username=foo&scope=hello+world&password=bar'
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
- return prepare_token_request('password', body=body, username=username,
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ return prepare_token_request(self.grant_type, body=body, username=username,
password=password, scope=scope, **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py
index 490efcd..11c6c51 100644
--- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py
@@ -45,6 +45,8 @@ class MobileApplicationClient(Client):
redirection URI, it may be exposed to the resource owner and other
applications residing on the same device.
"""
+
+ response_type = 'token'
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
state=None, **kwargs):
@@ -85,13 +87,13 @@ class MobileApplicationClient(Client):
>>> client.prepare_request_uri('https://example.com', foo='bar')
'https://example.com?client_id=your_id&response_type=token&foo=bar'
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
- .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
- .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
- return prepare_grant_uri(uri, self.client_id, 'token',
+ return prepare_grant_uri(uri, self.client_id, self.response_type,
redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
def parse_request_uri_response(self, uri, state=None, scope=None):
@@ -164,9 +166,9 @@ class MobileApplicationClient(Client):
>>> client.parse_request_body_response(response_body, scope=['other'])
('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
- .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
"""
self.token = parse_implicit_response(uri, state=state, scope=scope)
- self._populate_attributes(self.token)
+ self.populate_token_attributes(self.token)
return self.token
diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py
index e6c3270..35333d8 100644
--- a/oauthlib/oauth2/rfc6749/clients/service_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/service_application.py
@@ -54,7 +54,7 @@ class ServiceApplicationClient(Client):
``https://provider.com/oauth2/token``.
:param kwargs: Additional arguments to pass to base client, such as
- state and token. See Client.__init__.__doc__ for
+ state and token. See ``Client.__init__.__doc__`` for
details.
"""
super(ServiceApplicationClient, self).__init__(client_id, **kwargs)
@@ -72,7 +72,8 @@ class ServiceApplicationClient(Client):
issued_at=None,
extra_claims=None,
body='',
- scope=None,
+ scope=None,
+ include_client_id=None,
**kwargs):
"""Create and add a JWT assertion to the request body.
@@ -97,20 +98,32 @@ class ServiceApplicationClient(Client):
:param issued_at: A unix timestamp of when the JWT was created.
Defaults to now, i.e. ``time.time()``.
- :param not_before: A unix timestamp after which the JWT may be used.
- Not included unless provided.
-
- :param jwt_id: A unique JWT token identifier. Not included unless
- provided.
-
:param extra_claims: A dict of additional claims to include in the JWT.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
+
:param scope: The scope of the access request.
- :param body: Request body (string) with extra parameters.
+ :param include_client_id: `True` to send the `client_id` in the body of
+ the upstream request. Default `None`. This is
+ required if the client is not authenticating
+ with the authorization server as described
+ in `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
+ :param not_before: A unix timestamp after which the JWT may be used.
+ Not included unless provided. *
+
+ :param jwt_id: A unique JWT token identifier. Not included unless
+ provided. *
:param kwargs: Extra credentials to include in the token request.
+ Parameters marked with a `*` above are not explicit arguments in the
+ function signature, but are specially documented arguments for items
+ appearing in the generic `**kwargs` keyworded input.
+
The "scope" parameter may be used, as defined in the Assertion
Framework for OAuth 2.0 Client Authentication and Authorization Grants
[I-D.ietf-oauth-assertions] specification, to indicate the requested
@@ -136,7 +149,7 @@ class ServiceApplicationClient(Client):
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]
- .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
import jwt
@@ -146,8 +159,8 @@ class ServiceApplicationClient(Client):
' token requests.')
claim = {
'iss': issuer or self.issuer,
- 'aud': audience or self.issuer,
- 'sub': subject or self.issuer,
+ 'aud': audience or self.audience,
+ 'sub': subject or self.subject,
'exp': int(expires_at or time.time() + 3600),
'iat': int(issued_at or time.time()),
}
@@ -168,6 +181,8 @@ class ServiceApplicationClient(Client):
assertion = jwt.encode(claim, key, 'RS256')
assertion = to_unicode(assertion)
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
return prepare_token_request(self.grant_type,
body=body,
assertion=assertion,
diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py
index c099d99..0cd39ce 100644
--- a/oauthlib/oauth2/rfc6749/clients/web_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/web_application.py
@@ -8,6 +8,8 @@ for consuming and providing OAuth 2.0 RFC6749.
"""
from __future__ import absolute_import, unicode_literals
+import warnings
+
from ..parameters import (parse_authorization_code_response,
parse_token_response, prepare_grant_uri,
prepare_token_request)
@@ -32,6 +34,8 @@ class WebApplicationClient(Client):
browser) and capable of receiving incoming requests (via redirection)
from the authorization server.
"""
+
+ grant_type = 'authorization_code'
def __init__(self, client_id, code=None, **kwargs):
super(WebApplicationClient, self).__init__(client_id, **kwargs)
@@ -76,26 +80,23 @@ class WebApplicationClient(Client):
>>> client.prepare_request_uri('https://example.com', foo='bar')
'https://example.com?client_id=your_id&response_type=code&foo=bar'
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
- .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
- .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
return prepare_grant_uri(uri, self.client_id, 'code',
redirect_uri=redirect_uri, scope=scope, state=state, **kwargs)
- def prepare_request_body(self, client_id=None, code=None, body='',
- redirect_uri=None, **kwargs):
+ def prepare_request_body(self, code=None, redirect_uri=None, body='',
+ include_client_id=True, **kwargs):
"""Prepare the access token request body.
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format in the HTTP request entity-body:
- :param client_id: REQUIRED, if the client is not authenticating with the
- authorization server as described in `Section 3.2.1`_.
-
:param code: REQUIRED. The authorization code received from the
authorization server.
@@ -103,6 +104,15 @@ class WebApplicationClient(Client):
authorization request as described in `Section 4.1.1`_, and their
values MUST be identical.
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
+
+ :param include_client_id: `True` (default) to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
:param kwargs: Extra parameters to include in the token request.
In addition OAuthLib will add the ``grant_type`` parameter set to
@@ -120,12 +130,31 @@ class WebApplicationClient(Client):
>>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
- .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
- .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ `Section 3.2.1` also states:
+ In the "authorization_code" "grant_type" request to the token
+ endpoint, an unauthenticated client MUST send its "client_id" to
+ prevent itself from inadvertently accepting a code intended for a
+ client with a different "client_id". This protects the client from
+ substitution of the authentication code. (It provides no additional
+ security for the protected resource.)
+
+ .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
code = code or self.code
- return prepare_token_request('authorization_code', code=code, body=body,
- client_id=self.client_id, redirect_uri=redirect_uri, **kwargs)
+ if 'client_id' in kwargs:
+ warnings.warn("`client_id` has been deprecated in favor of "
+ "`include_client_id`, a boolean value which will "
+ "include the already configured `self.client_id`.",
+ DeprecationWarning)
+ if kwargs['client_id'] != self.client_id:
+ raise ValueError("`client_id` was supplied as an argument, but "
+ "it does not match `self.client_id`")
+
+ kwargs['client_id'] = self.client_id
+ kwargs['include_client_id'] = include_client_id
+ return prepare_token_request(self.grant_type, code=code, body=body,
+ redirect_uri=redirect_uri, **kwargs)
def parse_request_uri_response(self, uri, state=None):
"""Parse the URI query for code and state.
@@ -172,5 +201,5 @@ class WebApplicationClient(Client):
oauthlib.oauth2.rfc6749.errors.MismatchingStateError
"""
response = parse_authorization_code_response(uri, state=state)
- self._populate_attributes(response)
+ self.populate_code_attributes(response)
return response
diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py
index 848bec6..51e173d 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/__init__.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/__init__.py
@@ -9,6 +9,8 @@ for consuming and providing OAuth 2.0 RFC6749.
from __future__ import absolute_import, unicode_literals
from .authorization import AuthorizationEndpoint
+from .introspect import IntrospectEndpoint
+from .metadata import MetadataEndpoint
from .token import TokenEndpoint
from .resource import ResourceEndpoint
from .revocation import RevocationEndpoint
diff --git a/oauthlib/oauth2/rfc6749/endpoints/authorization.py b/oauthlib/oauth2/rfc6749/endpoints/authorization.py
index b6e0734..92cde34 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/authorization.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/authorization.py
@@ -59,7 +59,7 @@ class AuthorizationEndpoint(BaseEndpoint):
# Enforced through the design of oauthlib.common.Request
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
"""
def __init__(self, default_response_type, default_token_type,
diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py
index cdb015f..c0fc726 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/base.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/base.py
@@ -12,7 +12,8 @@ import functools
import logging
from ..errors import (FatalClientError, OAuth2Error, ServerError,
- TemporarilyUnavailableError)
+ TemporarilyUnavailableError, InvalidRequestError,
+ InvalidClientError, UnsupportedTokenTypeError)
log = logging.getLogger(__name__)
@@ -39,6 +40,28 @@ class BaseEndpoint(object):
def catch_errors(self, catch_errors):
self._catch_errors = catch_errors
+ def _raise_on_missing_token(self, request):
+ """Raise error on missing token."""
+ if not request.token:
+ raise InvalidRequestError(request=request,
+ description='Missing token parameter.')
+ def _raise_on_invalid_client(self, request):
+ """Raise on failed client authentication."""
+ if self.request_validator.client_authentication_required(request):
+ if not self.request_validator.authenticate_client(request):
+ log.debug('Client authentication failed, %r.', request)
+ raise InvalidClientError(request=request)
+ elif not self.request_validator.authenticate_client_id(request.client_id, request):
+ log.debug('Client authentication failed, %r.', request)
+ raise InvalidClientError(request=request)
+
+ def _raise_on_unsupported_token(self, request):
+ """Raise on unsupported tokens."""
+ if (request.token_type_hint and
+ request.token_type_hint in self.valid_token_types and
+ request.token_type_hint not in self.supported_token_types):
+ raise UnsupportedTokenTypeError(request=request)
+
def catch_errors_and_unavailability(f):
@functools.wraps(f)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/introspect.py b/oauthlib/oauth2/rfc6749/endpoints/introspect.py
new file mode 100644
index 0000000..47022fd
--- /dev/null
+++ b/oauthlib/oauth2/rfc6749/endpoints/introspect.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.endpoint.introspect
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the OAuth 2.0 `Token Introspection`.
+
+.. _`Token Introspection`: https://tools.ietf.org/html/rfc7662
+"""
+from __future__ import absolute_import, unicode_literals
+
+import json
+import logging
+
+from oauthlib.common import Request
+
+from ..errors import OAuth2Error, UnsupportedTokenTypeError
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class IntrospectEndpoint(BaseEndpoint):
+
+ """Introspect token endpoint.
+
+ This endpoint defines a method to query an OAuth 2.0 authorization
+ server to determine the active state of an OAuth 2.0 token and to
+ determine meta-information about this token. OAuth 2.0 deployments
+ can use this method to convey information about the authorization
+ context of the token from the authorization server to the protected
+ resource.
+
+ To prevent the values of access tokens from leaking into
+ server-side logs via query parameters, an authorization server
+ offering token introspection MAY disallow the use of HTTP GET on
+ the introspection endpoint and instead require the HTTP POST method
+ to be used at the introspection endpoint.
+ """
+
+ valid_token_types = ('access_token', 'refresh_token')
+
+ def __init__(self, request_validator, supported_token_types=None):
+ BaseEndpoint.__init__(self)
+ self.request_validator = request_validator
+ self.supported_token_types = (
+ supported_token_types or self.valid_token_types)
+
+ @catch_errors_and_unavailability
+ def create_introspect_response(self, uri, http_method='POST', body=None,
+ headers=None):
+ """Create introspect valid or invalid response
+
+ If the authorization server is unable to determine the state
+ of the token without additional information, it SHOULD return
+ an introspection response indicating the token is not active
+ as described in Section 2.2.
+ """
+ resp_headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+ request = Request(uri, http_method, body, headers)
+ try:
+ self.validate_introspect_request(request)
+ log.debug('Token introspect valid for %r.', request)
+ except OAuth2Error as e:
+ log.debug('Client error during validation of %r. %r.', request, e)
+ resp_headers.update(e.headers)
+ return resp_headers, e.json, e.status_code
+
+ claims = self.request_validator.introspect_token(
+ request.token,
+ request.token_type_hint,
+ request
+ )
+ if claims is None:
+ return resp_headers, json.dumps(dict(active=False)), 200
+ if "active" in claims:
+ claims.pop("active")
+ return resp_headers, json.dumps(dict(active=True, **claims)), 200
+
+ def validate_introspect_request(self, request):
+ """Ensure the request is valid.
+
+ The protected resource calls the introspection endpoint using
+ an HTTP POST request with parameters sent as
+ "application/x-www-form-urlencoded".
+
+ token REQUIRED. The string value of the token.
+
+ token_type_hint OPTIONAL.
+ A hint about the type of the token submitted for
+ introspection. The protected resource MAY pass this parameter to
+ help the authorization server optimize the token lookup. If the
+ server is unable to locate the token using the given hint, it MUST
+ extend its search across all of its supported token types. An
+ authorization server MAY ignore this parameter, particularly if it
+ is able to detect the token type automatically.
+ * access_token: An Access Token as defined in [`RFC6749`],
+ `section 1.4`_
+
+ * refresh_token: A Refresh Token as defined in [`RFC6749`],
+ `section 1.5`_
+
+ The introspection endpoint MAY accept other OPTIONAL
+ parameters to provide further context to the query. For
+ instance, an authorization server may desire to know the IP
+ address of the client accessing the protected resource to
+ determine if the correct client is likely to be presenting the
+ token. The definition of this or any other parameters are
+ outside the scope of this specification, to be defined by
+ service documentation or extensions to this specification.
+
+ .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`RFC6749`: http://tools.ietf.org/html/rfc6749
+ """
+ self._raise_on_missing_token(request)
+ self._raise_on_invalid_client(request)
+ self._raise_on_unsupported_token(request)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/metadata.py b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
new file mode 100644
index 0000000..936e878
--- /dev/null
+++ b/oauthlib/oauth2/rfc6749/endpoints/metadata.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.endpoint.metadata
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the `OAuth 2.0 Authorization Server Metadata`.
+
+.. _`OAuth 2.0 Authorization Server Metadata`: https://tools.ietf.org/html/rfc8414
+"""
+from __future__ import absolute_import, unicode_literals
+
+import copy
+import json
+import logging
+
+from ....common import unicode_type
+from .base import BaseEndpoint, catch_errors_and_unavailability
+from .authorization import AuthorizationEndpoint
+from .introspect import IntrospectEndpoint
+from .token import TokenEndpoint
+from .revocation import RevocationEndpoint
+from .. import grant_types
+
+
+log = logging.getLogger(__name__)
+
+
+class MetadataEndpoint(BaseEndpoint):
+
+ """OAuth2.0 Authorization Server Metadata endpoint.
+
+ This specification generalizes the metadata format defined by
+ `OpenID Connect Discovery 1.0` in a way that is compatible
+ with OpenID Connect Discovery while being applicable to a wider set
+ of OAuth 2.0 use cases. This is intentionally parallel to the way
+ that OAuth 2.0 Dynamic Client Registration Protocol [`RFC7591`_]
+ generalized the dynamic client registration mechanisms defined by
+ OpenID Connect Dynamic Client Registration 1.0
+ in a way that is compatible with it.
+
+ .. _`OpenID Connect Discovery 1.0`: https://openid.net/specs/openid-connect-discovery-1_0.html
+ .. _`RFC7591`: https://tools.ietf.org/html/rfc7591
+ """
+
+ def __init__(self, endpoints, claims={}, raise_errors=True):
+ assert isinstance(claims, dict)
+ for endpoint in endpoints:
+ assert isinstance(endpoint, BaseEndpoint)
+
+ BaseEndpoint.__init__(self)
+ self.raise_errors = raise_errors
+ self.endpoints = endpoints
+ self.initial_claims = claims
+ self.claims = self.validate_metadata_server()
+
+ @catch_errors_and_unavailability
+ def create_metadata_response(self, uri, http_method='GET', body=None,
+ headers=None):
+ """Create metadata response
+ """
+ headers = {
+ 'Content-Type': 'application/json'
+ }
+ return headers, json.dumps(self.claims), 200
+
+ def validate_metadata(self, array, key, is_required=False, is_list=False, is_url=False, is_issuer=False):
+ if not self.raise_errors:
+ return
+
+ if key not in array:
+ if is_required:
+ raise ValueError("key {} is a mandatory metadata.".format(key))
+
+ elif is_issuer:
+ if not array[key].startswith("https"):
+ raise ValueError("key {}: {} must be an HTTPS URL".format(key, array[key]))
+ if "?" in array[key] or "&" in array[key] or "#" in array[key]:
+ raise ValueError("key {}: {} must not contain query or fragment components".format(key, array[key]))
+
+ elif is_url:
+ if not array[key].startswith("http"):
+ raise ValueError("key {}: {} must be an URL".format(key, array[key]))
+
+ elif is_list:
+ if not isinstance(array[key], list):
+ raise ValueError("key {}: {} must be an Array".format(key, array[key]))
+ for elem in array[key]:
+ if not isinstance(elem, unicode_type):
+ raise ValueError("array {}: {} must contains only string (not {})".format(key, array[key], elem))
+
+ def validate_metadata_token(self, claims, endpoint):
+ """
+ If the token endpoint is used in the grant type, the value of this
+ parameter MUST be the same as the value of the "grant_type"
+ parameter passed to the token endpoint defined in the grant type
+ definition.
+ """
+ self._grant_types.extend(endpoint._grant_types.keys())
+ claims.setdefault("token_endpoint_auth_methods_supported", ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "token_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "token_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "token_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_authorization(self, claims, endpoint):
+ claims.setdefault("response_types_supported",
+ list(filter(lambda x: x != "none", endpoint._response_types.keys())))
+ claims.setdefault("response_modes_supported", ["query", "fragment"])
+
+ # The OAuth2.0 Implicit flow is defined as a "grant type" but it is not
+ # using the "token" endpoint, as such, we have to add it explicitly to
+ # the list of "grant_types_supported" when enabled.
+ if "token" in claims["response_types_supported"]:
+ self._grant_types.append("implicit")
+
+ self.validate_metadata(claims, "response_types_supported", is_required=True, is_list=True)
+ self.validate_metadata(claims, "response_modes_supported", is_list=True)
+ if "code" in claims["response_types_supported"]:
+ code_grant = endpoint._response_types["code"]
+ if not isinstance(code_grant, grant_types.AuthorizationCodeGrant) and hasattr(code_grant, "default_grant"):
+ code_grant = code_grant.default_grant
+
+ claims.setdefault("code_challenge_methods_supported",
+ list(code_grant._code_challenge_methods.keys()))
+ self.validate_metadata(claims, "code_challenge_methods_supported", is_list=True)
+ self.validate_metadata(claims, "authorization_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_revocation(self, claims, endpoint):
+ claims.setdefault("revocation_endpoint_auth_methods_supported",
+ ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "revocation_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "revocation_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "revocation_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_introspection(self, claims, endpoint):
+ claims.setdefault("introspection_endpoint_auth_methods_supported",
+ ["client_secret_post", "client_secret_basic"])
+
+ self.validate_metadata(claims, "introspection_endpoint_auth_methods_supported", is_list=True)
+ self.validate_metadata(claims, "introspection_endpoint_auth_signing_alg_values_supported", is_list=True)
+ self.validate_metadata(claims, "introspection_endpoint", is_required=True, is_url=True)
+
+ def validate_metadata_server(self):
+ """
+ Authorization servers can have metadata describing their
+ configuration. The following authorization server metadata values
+ are used by this specification. More details can be found in
+ `RFC8414 section 2`_ :
+
+ issuer
+ REQUIRED
+
+ authorization_endpoint
+ URL of the authorization server's authorization endpoint
+ [`RFC6749#Authorization`_]. This is REQUIRED unless no grant types are supported
+ that use the authorization endpoint.
+
+ token_endpoint
+ URL of the authorization server's token endpoint [`RFC6749#Token`_]. This
+ is REQUIRED unless only the implicit grant type is supported.
+
+ scopes_supported
+ RECOMMENDED.
+
+ response_types_supported
+ REQUIRED.
+
+ * Other OPTIONAL fields:
+ jwks_uri
+ registration_endpoint
+ response_modes_supported
+
+ grant_types_supported
+ OPTIONAL. JSON array containing a list of the OAuth 2.0 grant
+ type values that this authorization server supports. The array
+ values used are the same as those used with the "grant_types"
+ parameter defined by "OAuth 2.0 Dynamic Client Registration
+ Protocol" [`RFC7591`_]. If omitted, the default value is
+ "["authorization_code", "implicit"]".
+
+ token_endpoint_auth_methods_supported
+
+ token_endpoint_auth_signing_alg_values_supported
+
+ service_documentation
+
+ ui_locales_supported
+
+ op_policy_uri
+
+ op_tos_uri
+
+ revocation_endpoint
+
+ revocation_endpoint_auth_methods_supported
+
+ revocation_endpoint_auth_signing_alg_values_supported
+
+ introspection_endpoint
+
+ introspection_endpoint_auth_methods_supported
+
+ introspection_endpoint_auth_signing_alg_values_supported
+
+ code_challenge_methods_supported
+
+ Additional authorization server metadata parameters MAY also be used.
+ Some are defined by other specifications, such as OpenID Connect
+ Discovery 1.0 [`OpenID.Discovery`_].
+
+ .. _`RFC8414 section 2`: https://tools.ietf.org/html/rfc8414#section-2
+ .. _`RFC6749#Authorization`: https://tools.ietf.org/html/rfc6749#section-3.1
+ .. _`RFC6749#Token`: https://tools.ietf.org/html/rfc6749#section-3.2
+ .. _`RFC7591`: https://tools.ietf.org/html/rfc7591
+ .. _`OpenID.Discovery`: https://openid.net/specs/openid-connect-discovery-1_0.html
+ """
+ claims = copy.deepcopy(self.initial_claims)
+ self.validate_metadata(claims, "issuer", is_required=True, is_issuer=True)
+ self.validate_metadata(claims, "jwks_uri", is_url=True)
+ self.validate_metadata(claims, "scopes_supported", is_list=True)
+ self.validate_metadata(claims, "service_documentation", is_url=True)
+ self.validate_metadata(claims, "ui_locales_supported", is_list=True)
+ self.validate_metadata(claims, "op_policy_uri", is_url=True)
+ self.validate_metadata(claims, "op_tos_uri", is_url=True)
+
+ self._grant_types = []
+ for endpoint in self.endpoints:
+ if isinstance(endpoint, TokenEndpoint):
+ self.validate_metadata_token(claims, endpoint)
+ if isinstance(endpoint, AuthorizationEndpoint):
+ self.validate_metadata_authorization(claims, endpoint)
+ if isinstance(endpoint, RevocationEndpoint):
+ self.validate_metadata_revocation(claims, endpoint)
+ if isinstance(endpoint, IntrospectEndpoint):
+ self.validate_metadata_introspection(claims, endpoint)
+
+ # "grant_types_supported" is a combination of all OAuth2 grant types
+ # allowed in the current provider implementation.
+ claims.setdefault("grant_types_supported", self._grant_types)
+ self.validate_metadata(claims, "grant_types_supported", is_list=True)
+ return claims
diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
index 07c3715..e2cc9db 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
@@ -1,30 +1,28 @@
# -*- coding: utf-8 -*-
"""
-oauthlib.oauth2.rfc6749
-~~~~~~~~~~~~~~~~~~~~~~~
+oauthlib.oauth2.rfc6749.endpoints.pre_configured
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-This module is an implementation of various logic needed
-for consuming and providing OAuth 2.0 RFC6749.
+This module is an implementation of various endpoints needed
+for providing OAuth 2.0 RFC6749 servers.
"""
from __future__ import absolute_import, unicode_literals
-from ..grant_types import (AuthCodeGrantDispatcher, AuthorizationCodeGrant,
- AuthTokenGrantDispatcher,
+from ..grant_types import (AuthorizationCodeGrant,
ClientCredentialsGrant,
- ImplicitTokenGrantDispatcher, ImplicitGrant,
- OpenIDConnectAuthCode, OpenIDConnectImplicit,
- OpenIDConnectHybrid,
+ ImplicitGrant,
RefreshTokenGrant,
ResourceOwnerPasswordCredentialsGrant)
from ..tokens import BearerToken
from .authorization import AuthorizationEndpoint
+from .introspect import IntrospectEndpoint
from .resource import ResourceEndpoint
from .revocation import RevocationEndpoint
from .token import TokenEndpoint
-class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
- RevocationEndpoint):
+class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring all four major grant types."""
@@ -50,36 +48,21 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
request_validator)
credentials_grant = ClientCredentialsGrant(request_validator)
refresh_grant = RefreshTokenGrant(request_validator)
- openid_connect_auth = OpenIDConnectAuthCode(request_validator)
- openid_connect_implicit = OpenIDConnectImplicit(request_validator)
- openid_connect_hybrid = OpenIDConnectHybrid(request_validator)
bearer = BearerToken(request_validator, token_generator,
token_expires_in, refresh_token_generator)
- auth_grant_choice = AuthCodeGrantDispatcher(default_auth_grant=auth_grant, oidc_auth_grant=openid_connect_auth)
- implicit_grant_choice = ImplicitTokenGrantDispatcher(default_implicit_grant=implicit_grant, oidc_implicit_grant=openid_connect_implicit)
-
- # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations
- # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination
AuthorizationEndpoint.__init__(self, default_response_type='code',
response_types={
- 'code': auth_grant_choice,
- 'token': implicit_grant_choice,
- 'id_token': openid_connect_implicit,
- 'id_token token': openid_connect_implicit,
- 'code token': openid_connect_hybrid,
- 'code id_token': openid_connect_hybrid,
- 'code id_token token': openid_connect_hybrid,
+ 'code': auth_grant,
+ 'token': implicit_grant,
'none': auth_grant
},
default_token_type=bearer)
- token_grant_choice = AuthTokenGrantDispatcher(request_validator, default_token_grant=auth_grant, oidc_token_grant=openid_connect_auth)
-
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
grant_types={
- 'authorization_code': token_grant_choice,
+ 'authorization_code': auth_grant,
'password': password_grant,
'client_credentials': credentials_grant,
'refresh_token': refresh_grant,
@@ -88,10 +71,11 @@ class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
-class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
- RevocationEndpoint):
+class WebApplicationServer(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
@@ -126,10 +110,11 @@ class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoin
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
-class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint,
- RevocationEndpoint):
+class MobileApplicationServer(AuthorizationEndpoint, IntrospectEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
@@ -159,10 +144,12 @@ class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint,
token_types={'Bearer': bearer})
RevocationEndpoint.__init__(self, request_validator,
supported_token_types=['access_token'])
+ IntrospectEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
-class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint,
- RevocationEndpoint):
+class LegacyApplicationServer(TokenEndpoint, IntrospectEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
@@ -195,10 +182,11 @@ class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint,
ResourceEndpoint.__init__(self, default_token='Bearer',
token_types={'Bearer': bearer})
RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
-class BackendApplicationServer(TokenEndpoint, ResourceEndpoint,
- RevocationEndpoint):
+class BackendApplicationServer(TokenEndpoint, IntrospectEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
"""An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
@@ -228,3 +216,5 @@ class BackendApplicationServer(TokenEndpoint, ResourceEndpoint,
token_types={'Bearer': bearer})
RevocationEndpoint.__init__(self, request_validator,
supported_token_types=['access_token'])
+ IntrospectEndpoint.__init__(self, request_validator,
+ supported_token_types=['access_token'])
diff --git a/oauthlib/oauth2/rfc6749/endpoints/resource.py b/oauthlib/oauth2/rfc6749/endpoints/resource.py
index d03ed21..f19c60c 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/resource.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/resource.py
@@ -83,5 +83,5 @@ class ResourceEndpoint(BaseEndpoint):
to give an estimation based on the request.
"""
estimates = sorted(((t.estimate_type(request), n)
- for n, t in self.tokens.items()))
+ for n, t in self.tokens.items()), reverse=True)
return estimates[0][1] if len(estimates) else None
diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py
index 4364b81..fda3f30 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/revocation.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/revocation.py
@@ -5,7 +5,7 @@ oauthlib.oauth2.rfc6749.endpoint.revocation
An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11).
-.. _`Token Revocation`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11
+.. _`Token Revocation`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11
"""
from __future__ import absolute_import, unicode_literals
@@ -13,8 +13,7 @@ import logging
from oauthlib.common import Request
-from ..errors import (InvalidClientError, InvalidRequestError, OAuth2Error,
- UnsupportedTokenTypeError)
+from ..errors import OAuth2Error, UnsupportedTokenTypeError
from .base import BaseEndpoint, catch_errors_and_unavailability
log = logging.getLogger(__name__)
@@ -59,6 +58,11 @@ class RevocationEndpoint(BaseEndpoint):
An invalid token type hint value is ignored by the authorization server
and does not influence the revocation response.
"""
+ resp_headers = {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
request = Request(
uri, http_method=http_method, body=body, headers=headers)
try:
@@ -69,7 +73,8 @@ class RevocationEndpoint(BaseEndpoint):
response_body = e.json
if self.enable_jsonp and request.callback:
response_body = '%s(%s);' % (request.callback, response_body)
- return {}, response_body, e.status_code
+ resp_headers.update(e.headers)
+ return resp_headers, response_body, e.status_code
self.request_validator.revoke_token(request.token,
request.token_type_hint, request)
@@ -110,25 +115,12 @@ class RevocationEndpoint(BaseEndpoint):
The client also includes its authentication credentials as described in
`Section 2.3`_. of [`RFC6749`_].
- .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
- .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
- .. _`section 2.3`: http://tools.ietf.org/html/rfc6749#section-2.3
- .. _`Section 4.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
- .. _`RFC6749`: http://tools.ietf.org/html/rfc6749
+ .. _`section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`section 2.3`: https://tools.ietf.org/html/rfc6749#section-2.3
+ .. _`Section 4.1.2`: https://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
+ .. _`RFC6749`: https://tools.ietf.org/html/rfc6749
"""
- if not request.token:
- raise InvalidRequestError(request=request,
- description='Missing token parameter.')
-
- if self.request_validator.client_authentication_required(request):
- if not self.request_validator.authenticate_client(request):
- log.debug('Client authentication failed, %r.', request)
- raise InvalidClientError(request=request)
- elif not self.request_validator.authenticate_client_id(request.client_id, request):
- log.debug('Client authentication failed, %r.', request)
- raise InvalidClientError(request=request)
-
- if (request.token_type_hint and
- request.token_type_hint in self.valid_token_types and
- request.token_type_hint not in self.supported_token_types):
- raise UnsupportedTokenTypeError(request=request)
+ self._raise_on_missing_token(request)
+ self._raise_on_invalid_client(request)
+ self._raise_on_unsupported_token(request)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py
index ece6325..90fb16f 100644
--- a/oauthlib/oauth2/rfc6749/endpoints/token.py
+++ b/oauthlib/oauth2/rfc6749/endpoints/token.py
@@ -59,7 +59,7 @@ class TokenEndpoint(BaseEndpoint):
# Delegated to each grant type.
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
"""
def __init__(self, default_grant_type, default_token_type, grant_types):
diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py
index 180f636..d2a1402 100644
--- a/oauthlib/oauth2/rfc6749/errors.py
+++ b/oauthlib/oauth2/rfc6749/errors.py
@@ -21,23 +21,26 @@ class OAuth2Error(Exception):
def __init__(self, description=None, uri=None, state=None,
status_code=None, request=None):
"""
- description: A human-readable ASCII [USASCII] text providing
- additional information, used to assist the client
- developer in understanding the error that occurred.
- Values for the "error_description" parameter MUST NOT
- include characters outside the set
- x20-21 / x23-5B / x5D-7E.
-
- uri: A URI identifying a human-readable web page with information
- about the error, used to provide the client developer with
- additional information about the error. Values for the
- "error_uri" parameter MUST conform to the URI- Reference
- syntax, and thus MUST NOT include characters outside the set
- x21 / x23-5B / x5D-7E.
-
- state: A CSRF protection value received from the client.
-
- request: Oauthlib Request object
+ :param description: A human-readable ASCII [USASCII] text providing
+ additional information, used to assist the client
+ developer in understanding the error that occurred.
+ Values for the "error_description" parameter
+ MUST NOT include characters outside the set
+ x20-21 / x23-5B / x5D-7E.
+
+ :param uri: A URI identifying a human-readable web page with information
+ about the error, used to provide the client developer with
+ additional information about the error. Values for the
+ "error_uri" parameter MUST conform to the URI- Reference
+ syntax, and thus MUST NOT include characters outside the set
+ x21 / x23-5B / x5D-7E.
+
+ :param state: A CSRF protection value received from the client.
+
+ :param status_code:
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
"""
if description is not None:
self.description = description
@@ -93,6 +96,27 @@ class OAuth2Error(Exception):
def json(self):
return json.dumps(dict(self.twotuples))
+ @property
+ def headers(self):
+ if self.status_code == 401:
+ """
+ https://tools.ietf.org/html/rfc6750#section-3
+
+ All challenges defined by this specification MUST use the auth-scheme
+ value "Bearer". This scheme MUST be followed by one or more
+ auth-param values.
+ """
+ authvalues = [
+ "Bearer",
+ 'error="{}"'.format(self.error)
+ ]
+ if self.description:
+ authvalues.append('error_description="{}"'.format(self.description))
+ if self.uri:
+ authvalues.append('error_uri="{}"'.format(self.uri))
+ return {"WWW-Authenticate": ", ".join(authvalues)}
+ return {}
+
class TokenExpiredError(OAuth2Error):
error = 'token_expired'
@@ -177,12 +201,31 @@ class MissingResponseTypeError(InvalidRequestError):
description = 'Missing response_type parameter.'
+class MissingCodeChallengeError(InvalidRequestError):
+ """
+ If the server requires Proof Key for Code Exchange (PKCE) by OAuth
+ public clients and the client does not send the "code_challenge" in
+ the request, the authorization endpoint MUST return the authorization
+ error response with the "error" value set to "invalid_request". The
+ "error_description" or the response of "error_uri" SHOULD explain the
+ nature of error, e.g., code challenge required.
+ """
+ description = 'Code challenge required.'
+
+
+class MissingCodeVerifierError(InvalidRequestError):
+ """
+ The request to the token endpoint, when PKCE is enabled, has
+ the parameter `code_verifier` REQUIRED.
+ """
+ description = 'Code verifier required.'
+
+
class AccessDeniedError(OAuth2Error):
"""
The resource owner or authorization server denied the request.
"""
error = 'access_denied'
- status_code = 401
class UnsupportedResponseTypeError(OAuth2Error):
@@ -193,12 +236,26 @@ class UnsupportedResponseTypeError(OAuth2Error):
error = 'unsupported_response_type'
+class UnsupportedCodeChallengeMethodError(InvalidRequestError):
+ """
+ If the server supporting PKCE does not support the requested
+ transformation, the authorization endpoint MUST return the
+ authorization error response with "error" value set to
+ "invalid_request". The "error_description" or the response of
+ "error_uri" SHOULD explain the nature of error, e.g., transform
+ algorithm not supported.
+ """
+ description = 'Transform algorithm not supported.'
+
+
class InvalidScopeError(OAuth2Error):
"""
- The requested scope is invalid, unknown, or malformed.
+ The requested scope is invalid, unknown, or malformed, or
+ exceeds the scope granted by the resource owner.
+
+ https://tools.ietf.org/html/rfc6749#section-5.2
"""
error = 'invalid_scope'
- status_code = 401
class ServerError(OAuth2Error):
@@ -221,7 +278,7 @@ class TemporarilyUnavailableError(OAuth2Error):
error = 'temporarily_unavailable'
-class InvalidClientError(OAuth2Error):
+class InvalidClientError(FatalClientError):
"""
Client authentication failed (e.g. unknown client, no client
authentication included, or unsupported authentication method).
@@ -243,9 +300,11 @@ class InvalidGrantError(OAuth2Error):
owner credentials) or refresh token is invalid, expired, revoked, does
not match the redirection URI used in the authorization request, or was
issued to another client.
+
+ https://tools.ietf.org/html/rfc6749#section-5.2
"""
error = 'invalid_grant'
- status_code = 401
+ status_code = 400
class UnauthorizedClientError(OAuth2Error):
@@ -254,7 +313,6 @@ class UnauthorizedClientError(OAuth2Error):
grant type.
"""
error = 'unauthorized_client'
- status_code = 401
class UnsupportedGrantTypeError(OAuth2Error):
@@ -267,113 +325,13 @@ class UnsupportedGrantTypeError(OAuth2Error):
class UnsupportedTokenTypeError(OAuth2Error):
"""
- The authorization server does not support the revocation of the
+ The authorization server does not support the hint of the
presented token type. I.e. the client tried to revoke an access token
on a server not supporting this feature.
"""
error = 'unsupported_token_type'
-class FatalOpenIDClientError(FatalClientError):
- pass
-
-
-class OpenIDClientError(OAuth2Error):
- pass
-
-
-class InteractionRequired(OpenIDClientError):
- """
- The Authorization Server requires End-User interaction to proceed.
-
- This error MAY be returned when the prompt parameter value in the
- Authentication Request is none, but the Authentication Request cannot be
- completed without displaying a user interface for End-User interaction.
- """
- error = 'interaction_required'
- status_code = 401
-
-
-class LoginRequired(OpenIDClientError):
- """
- The Authorization Server requires End-User authentication.
-
- This error MAY be returned when the prompt parameter value in the
- Authentication Request is none, but the Authentication Request cannot be
- completed without displaying a user interface for End-User authentication.
- """
- error = 'login_required'
- status_code = 401
-
-
-class AccountSelectionRequired(OpenIDClientError):
- """
- The End-User is REQUIRED to select a session at the Authorization Server.
-
- The End-User MAY be authenticated at the Authorization Server with
- different associated accounts, but the End-User did not select a session.
- This error MAY be returned when the prompt parameter value in the
- Authentication Request is none, but the Authentication Request cannot be
- completed without displaying a user interface to prompt for a session to
- use.
- """
- error = 'account_selection_required'
-
-
-class ConsentRequired(OpenIDClientError):
- """
- The Authorization Server requires End-User consent.
-
- This error MAY be returned when the prompt parameter value in the
- Authentication Request is none, but the Authentication Request cannot be
- completed without displaying a user interface for End-User consent.
- """
- error = 'consent_required'
- status_code = 401
-
-
-class InvalidRequestURI(OpenIDClientError):
- """
- The request_uri in the Authorization Request returns an error or
- contains invalid data.
- """
- error = 'invalid_request_uri'
- description = 'The request_uri in the Authorization Request returns an ' \
- 'error or contains invalid data.'
-
-
-class InvalidRequestObject(OpenIDClientError):
- """
- The request parameter contains an invalid Request Object.
- """
- error = 'invalid_request_object'
- description = 'The request parameter contains an invalid Request Object.'
-
-
-class RequestNotSupported(OpenIDClientError):
- """
- The OP does not support use of the request parameter.
- """
- error = 'request_not_supported'
- description = 'The request parameter is not supported.'
-
-
-class RequestURINotSupported(OpenIDClientError):
- """
- The OP does not support use of the request_uri parameter.
- """
- error = 'request_uri_not_supported'
- description = 'The request_uri parameter is not supported.'
-
-
-class RegistrationNotSupported(OpenIDClientError):
- """
- The OP does not support use of the registration parameter.
- """
- error = 'registration_not_supported'
- description = 'The registration parameter is not supported.'
-
-
class InvalidTokenError(OAuth2Error):
"""
The access token provided is expired, revoked, malformed, or
@@ -402,6 +360,38 @@ class InsufficientScopeError(OAuth2Error):
"the access token.")
+class ConsentRequired(OAuth2Error):
+ """
+ The Authorization Server requires End-User consent.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User consent.
+ """
+ error = 'consent_required'
+
+
+class LoginRequired(OAuth2Error):
+ """
+ The Authorization Server requires End-User authentication.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User authentication.
+ """
+ error = 'login_required'
+
+
+class CustomOAuth2Error(OAuth2Error):
+ """
+ This error is a placeholder for all custom errors not described by the RFC.
+ Some of the popular OAuth2 providers are using custom errors.
+ """
+ def __init__(self, error, *args, **kwargs):
+ self.error = error
+ super(CustomOAuth2Error, self).__init__(*args, **kwargs)
+
+
def raise_from_error(error, params=None):
import inspect
import sys
@@ -413,3 +403,4 @@ def raise_from_error(error, params=None):
for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
if cls.error == error:
raise cls(**kwargs)
+ raise CustomOAuth2Error(error=error, **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py
index 2e4bfe4..2ec8e4f 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/__init__.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/__init__.py
@@ -10,11 +10,3 @@ from .implicit import ImplicitGrant
from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant
from .client_credentials import ClientCredentialsGrant
from .refresh_token import RefreshTokenGrant
-from .openid_connect import OpenIDConnectBase
-from .openid_connect import OpenIDConnectAuthCode
-from .openid_connect import OpenIDConnectImplicit
-from .openid_connect import OpenIDConnectHybrid
-from .openid_connect import OIDCNoPrompt
-from .openid_connect import AuthCodeGrantDispatcher
-from .openid_connect import AuthTokenGrantDispatcher
-from .openid_connect import ImplicitTokenGrantDispatcher
diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
index 8661c35..6463391 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
@@ -5,11 +5,12 @@ oauthlib.oauth2.rfc6749.grant_types
"""
from __future__ import absolute_import, unicode_literals
+import base64
+import hashlib
import json
import logging
from oauthlib import common
-from oauthlib.uri_validate import is_absolute_uri
from .. import errors
from .base import GrantTypeBase
@@ -17,6 +18,52 @@ from .base import GrantTypeBase
log = logging.getLogger(__name__)
+def code_challenge_method_s256(verifier, challenge):
+ """
+ If the "code_challenge_method" from `Section 4.3`_ was "S256", the
+ received "code_verifier" is hashed by SHA-256, base64url-encoded, and
+ then compared to the "code_challenge", i.e.:
+
+ BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
+
+ How to implement a base64url-encoding
+ function without padding, based upon the standard base64-encoding
+ function that uses padding.
+
+ To be concrete, example C# code implementing these functions is shown
+ below. Similar code could be used in other languages.
+
+ static string base64urlencode(byte [] arg)
+ {
+ string s = Convert.ToBase64String(arg); // Regular base64 encoder
+ s = s.Split('=')[0]; // Remove any trailing '='s
+ s = s.Replace('+', '-'); // 62nd char of encoding
+ s = s.Replace('/', '_'); // 63rd char of encoding
+ return s;
+ }
+
+ In python urlsafe_b64encode is already replacing '+' and '/', but preserve
+ the trailing '='. So we have to remove it.
+
+ .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
+ """
+ return base64.urlsafe_b64encode(
+ hashlib.sha256(verifier.encode()).digest()
+ ).decode().rstrip('=') == challenge
+
+
+def code_challenge_method_plain(verifier, challenge):
+ """
+ If the "code_challenge_method" from `Section 4.3`_ was "plain", they are
+ compared directly, i.e.:
+
+ code_verifier == code_challenge.
+
+ .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
+ """
+ return verifier == challenge
+
+
class AuthorizationCodeGrant(GrantTypeBase):
"""`Authorization Code Grant`_
@@ -91,14 +138,35 @@ class AuthorizationCodeGrant(GrantTypeBase):
step (C). If valid, the authorization server responds back with
an access token and, optionally, a refresh token.
- .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are
+ susceptible to the authorization code interception attack.
+
+ A technique to mitigate against the threat through the use of Proof Key for Code
+ Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib
+ implementation.
+
+ .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1
+ .. _`PKCE`: https://tools.ietf.org/html/rfc7636
"""
default_response_mode = 'query'
response_types = ['code']
+ # This dict below is private because as RFC mention it:
+ # "S256" is Mandatory To Implement (MTI) on the server.
+ #
+ _code_challenge_methods = {
+ 'plain': code_challenge_method_plain,
+ 'S256': code_challenge_method_s256
+ }
+
def create_authorization_code(self, request):
- """Generates an authorization grant represented as a dictionary."""
+ """
+ Generates an authorization grant represented as a dictionary.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
grant = {'code': common.generate_token()}
if hasattr(request, 'state') and request.state:
grant['state'] = request.state
@@ -135,12 +203,12 @@ class AuthorizationCodeGrant(GrantTypeBase):
HTTP redirection response, or by other means available to it via the
user-agent.
- :param request: oauthlib.commong.Request
- :param token_handler: A token handler instace, for example of type
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
oauthlib.oauth2.BearerToken.
:returns: headers, body, status
:raises: FatalClientError on invalid redirect URI or client id.
- ValueError if scopes are not set on the request object.
A few examples::
@@ -151,12 +219,6 @@ class AuthorizationCodeGrant(GrantTypeBase):
>>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
>>> token = BearerToken(your_validator)
>>> grant = AuthorizationCodeGrant(your_validator)
- >>> grant.create_authorization_response(request, token)
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "oauthlib/oauth2/rfc6749/grant_types.py", line 513, in create_authorization_response
- raise ValueError('Scopes must be set on post auth.')
- ValueError: Scopes must be set on post auth.
>>> request.scopes = ['authorized', 'in', 'some', 'form']
>>> grant.create_authorization_response(request, token)
(u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
@@ -175,18 +237,13 @@ class AuthorizationCodeGrant(GrantTypeBase):
File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
- .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
- .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
try:
- # request.scopes is only mandated in post auth and both pre and
- # post auth use validate_authorization_request
- if not request.scopes:
- raise ValueError('Scopes must be set on post auth.')
-
self.validate_authorization_request(request)
log.debug('Pre resource owner authorization validation ok for %r.',
request)
@@ -206,7 +263,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
# the authorization server informs the client by adding the following
# parameters to the query component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B:
- # http://tools.ietf.org/html/rfc6749#appendix-B
+ # https://tools.ietf.org/html/rfc6749#appendix-B
except errors.OAuth2Error as e:
log.debug('Client error during validation of %r. %r.', request, e)
request.redirect_uri = request.redirect_uri or self.error_uri
@@ -232,17 +289,20 @@ class AuthorizationCodeGrant(GrantTypeBase):
MUST deny the request and SHOULD revoke (when possible) all tokens
previously issued based on that authorization code. The authorization
code is bound to the client identifier and redirection URI.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
"""
- headers = {
- 'Content-Type': 'application/json',
- 'Cache-Control': 'no-store',
- 'Pragma': 'no-cache',
- }
+ headers = self._get_default_headers()
try:
self.validate_token_request(request)
log.debug('Token request validation ok for %r.', request)
except errors.OAuth2Error as e:
log.debug('Client error during validation of %r. %r.', request, e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, refresh_token=self.refresh_token, save_token=False)
@@ -265,6 +325,9 @@ class AuthorizationCodeGrant(GrantTypeBase):
missing. These must be caught by the provider and handled, how this
is done is outside of the scope of OAuthLib but showing an error
page describing the issue is a good idea.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
"""
# First check for fatal errors
@@ -285,7 +348,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
# REQUIRED. The client identifier as described in Section 2.2.
- # http://tools.ietf.org/html/rfc6749#section-2.2
+ # https://tools.ietf.org/html/rfc6749#section-2.2
if not request.client_id:
raise errors.MissingClientIdError(request=request)
@@ -293,25 +356,13 @@ class AuthorizationCodeGrant(GrantTypeBase):
raise errors.InvalidClientIdError(request=request)
# OPTIONAL. As described in Section 3.1.2.
- # http://tools.ietf.org/html/rfc6749#section-3.1.2
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
log.debug('Validating redirection uri %s for client %s.',
request.redirect_uri, request.client_id)
- if request.redirect_uri is not None:
- request.using_default_redirect_uri = False
- log.debug('Using provided redirect_uri %s', request.redirect_uri)
- if not is_absolute_uri(request.redirect_uri):
- raise errors.InvalidRedirectURIError(request=request)
- if not self.request_validator.validate_redirect_uri(
- request.client_id, request.redirect_uri, request):
- raise errors.MismatchingRedirectURIError(request=request)
- else:
- request.redirect_uri = self.request_validator.get_default_redirect_uri(
- request.client_id, request)
- request.using_default_redirect_uri = True
- log.debug('Using default redirect_uri %s.', request.redirect_uri)
- if not request.redirect_uri:
- raise errors.MissingRedirectURIError(request=request)
+ # OPTIONAL. As described in Section 3.1.2.
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
+ self._handle_redirects(request)
# Then check for normal errors.
@@ -320,7 +371,7 @@ class AuthorizationCodeGrant(GrantTypeBase):
# the authorization server informs the client by adding the following
# parameters to the query component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B.
- # http://tools.ietf.org/html/rfc6749#appendix-B
+ # https://tools.ietf.org/html/rfc6749#appendix-B
# Note that the correct parameters to be added are automatically
# populated through the use of specific exceptions.
@@ -345,8 +396,22 @@ class AuthorizationCodeGrant(GrantTypeBase):
request.client_id, request.response_type)
raise errors.UnauthorizedClientError(request=request)
+ # OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request"
+ # https://tools.ietf.org/html/rfc6749#section-4.4.1
+ if self.request_validator.is_pkce_required(request.client_id, request) is True:
+ if request.code_challenge is None:
+ raise errors.MissingCodeChallengeError(request=request)
+
+ if request.code_challenge is not None:
+ # OPTIONAL, defaults to "plain" if not present in the request.
+ if request.code_challenge_method is None:
+ request.code_challenge_method = "plain"
+
+ if request.code_challenge_method not in self._code_challenge_methods:
+ raise errors.UnsupportedCodeChallengeMethodError(request=request)
+
# OPTIONAL. The scope of the access request as described by Section 3.3
- # http://tools.ietf.org/html/rfc6749#section-3.3
+ # https://tools.ietf.org/html/rfc6749#section-3.3
self.validate_scopes(request)
request_info.update({
@@ -363,6 +428,10 @@ class AuthorizationCodeGrant(GrantTypeBase):
return request.scopes, request_info
def validate_token_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
# REQUIRED. Value MUST be set to "authorization_code".
if request.grant_type not in ('authorization_code', 'openid'):
raise errors.UnsupportedGrantTypeError(request=request)
@@ -384,14 +453,14 @@ class AuthorizationCodeGrant(GrantTypeBase):
# credentials (or assigned other authentication requirements), the
# client MUST authenticate with the authorization server as described
# in Section 3.2.1.
- # http://tools.ietf.org/html/rfc6749#section-3.2.1
+ # https://tools.ietf.org/html/rfc6749#section-3.2.1
if not self.request_validator.authenticate_client(request):
log.debug('Client authentication failed, %r.', request)
raise errors.InvalidClientError(request=request)
elif not self.request_validator.authenticate_client_id(request.client_id, request):
# REQUIRED, if the client is not authenticating with the
# authorization server as described in Section 3.2.1.
- # http://tools.ietf.org/html/rfc6749#section-3.2.1
+ # https://tools.ietf.org/html/rfc6749#section-3.2.1
log.debug('Client authentication failed, %r.', request)
raise errors.InvalidClientError(request=request)
@@ -413,6 +482,33 @@ class AuthorizationCodeGrant(GrantTypeBase):
request.client_id, request.client, request.scopes)
raise errors.InvalidGrantError(request=request)
+ # OPTIONAL. Validate PKCE code_verifier
+ challenge = self.request_validator.get_code_challenge(request.code, request)
+
+ if challenge is not None:
+ if request.code_verifier is None:
+ raise errors.MissingCodeVerifierError(request=request)
+
+ challenge_method = self.request_validator.get_code_challenge_method(request.code, request)
+ if challenge_method is None:
+ raise errors.InvalidGrantError(request=request, description="Challenge method not found")
+
+ if challenge_method not in self._code_challenge_methods:
+ raise errors.ServerError(
+ description="code_challenge_method {} is not supported.".format(challenge_method),
+ request=request
+ )
+
+ if not self.validate_code_challenge(challenge,
+ challenge_method,
+ request.code_verifier):
+ log.debug('request provided a invalid code_verifier.')
+ raise errors.InvalidGrantError(request=request)
+ elif self.request_validator.is_pkce_required(request.client_id, request) is True:
+ if request.code_verifier is None:
+ raise errors.MissingCodeVerifierError(request=request)
+ raise errors.InvalidGrantError(request=request, description="Challenge not found")
+
for attr in ('user', 'scopes'):
if getattr(request, attr, None) is None:
log.debug('request.%s was not set on code validation.', attr)
@@ -420,11 +516,28 @@ class AuthorizationCodeGrant(GrantTypeBase):
# REQUIRED, if the "redirect_uri" parameter was included in the
# authorization request as described in Section 4.1.1, and their
# values MUST be identical.
+ if request.redirect_uri is None:
+ request.using_default_redirect_uri = True
+ request.redirect_uri = self.request_validator.get_default_redirect_uri(
+ request.client_id, request)
+ log.debug('Using default redirect_uri %s.', request.redirect_uri)
+ if not request.redirect_uri:
+ raise errors.MissingRedirectURIError(request=request)
+ else:
+ request.using_default_redirect_uri = False
+ log.debug('Using provided redirect_uri %s', request.redirect_uri)
+
if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
- request.redirect_uri, request.client):
+ request.redirect_uri, request.client,
+ request):
log.debug('Redirect_uri (%r) invalid for client %r (%r).',
request.redirect_uri, request.client_id, request.client)
raise errors.MismatchingRedirectURIError(request=request)
for validator in self.custom_validators.post_token:
validator(request)
+
+ def validate_code_challenge(self, challenge, challenge_method, verifier):
+ if challenge_method in self._code_challenge_methods:
+ return self._code_challenge_methods[challenge_method](verifier, challenge)
+ raise NotImplementedError('Unknown challenge_method %s' % challenge_method)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py
index e5d8ddd..f0772e2 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/base.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/base.py
@@ -9,51 +9,53 @@ import logging
from itertools import chain
from oauthlib.common import add_params_to_uri
+from oauthlib.uri_validate import is_absolute_uri
from oauthlib.oauth2.rfc6749 import errors, utils
from ..request_validator import RequestValidator
log = logging.getLogger(__name__)
+
class ValidatorsContainer(object):
"""
- Container object for holding custom validator callables to be invoked
- as part of the grant type `validate_authorization_request()` or
- `validate_authorization_request()` methods on the various grant types.
+ Container object for holding custom validator callables to be invoked
+ as part of the grant type `validate_authorization_request()` or
+ `validate_authorization_request()` methods on the various grant types.
- Authorization validators must be callables that take a request object and
- return a dict, which may contain items to be added to the `request_info`
- returned from the grant_type after validation.
+ Authorization validators must be callables that take a request object and
+ return a dict, which may contain items to be added to the `request_info`
+ returned from the grant_type after validation.
- Token validators must be callables that take a request object and
- return None.
+ Token validators must be callables that take a request object and
+ return None.
- Both authorization validators and token validators may raise OAuth2
- exceptions if validation conditions fail.
+ Both authorization validators and token validators may raise OAuth2
+ exceptions if validation conditions fail.
- Authorization validators added to `pre_auth` will be run BEFORE
- the standard validations (but after the critical ones that raise
- fatal errors) as part of `validate_authorization_request()`
+ Authorization validators added to `pre_auth` will be run BEFORE
+ the standard validations (but after the critical ones that raise
+ fatal errors) as part of `validate_authorization_request()`
- Authorization validators added to `post_auth` will be run AFTER
- the standard validations as part of `validate_authorization_request()`
+ Authorization validators added to `post_auth` will be run AFTER
+ the standard validations as part of `validate_authorization_request()`
- Token validators added to `pre_token` will be run BEFORE
- the standard validations as part of `validate_token_request()`
+ Token validators added to `pre_token` will be run BEFORE
+ the standard validations as part of `validate_token_request()`
- Token validators added to `post_token` will be run AFTER
- the standard validations as part of `validate_token_request()`
+ Token validators added to `post_token` will be run AFTER
+ the standard validations as part of `validate_token_request()`
- For example:
+ For example:
- >>> def my_auth_validator(request):
- ... return {'myval': True}
- >>> auth_code_grant = AuthorizationCodeGrant(request_validator)
- >>> auth_code_grant.custom_validators.pre_auth.append(my_auth_validator)
- >>> def my_token_validator(request):
- ... if not request.everything_okay:
- ... raise errors.OAuth2Error("uh-oh")
- >>> auth_code_grant.custom_validators.post_token.append(my_token_validator)
+ >>> def my_auth_validator(request):
+ ... return {'myval': True}
+ >>> auth_code_grant = AuthorizationCodeGrant(request_validator)
+ >>> auth_code_grant.custom_validators.pre_auth.append(my_auth_validator)
+ >>> def my_token_validator(request):
+ ... if not request.everything_okay:
+ ... raise errors.OAuth2Error("uh-oh")
+ >>> auth_code_grant.custom_validators.post_token.append(my_token_validator)
"""
def __init__(self, post_auth, post_token,
@@ -116,14 +118,32 @@ class GrantTypeBase(object):
def register_token_modifier(self, modifier):
self._token_modifiers.append(modifier)
-
def create_authorization_response(self, request, token_handler):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+ """
raise NotImplementedError('Subclasses must implement this method.')
def create_token_response(self, request, token_handler):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+ """
raise NotImplementedError('Subclasses must implement this method.')
def add_token(self, token, token_handler, request):
+ """
+ :param token:
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
# Only add a hybrid access token on auth step if asked for
if not request.response_type in ["token", "code token", "id_token token", "code id_token token"]:
return token
@@ -132,6 +152,10 @@ class GrantTypeBase(object):
return token
def validate_grant_type(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
client_id = getattr(request, 'client_id', None)
if not self.request_validator.validate_grant_type(client_id,
request.grant_type, request.client, request):
@@ -140,6 +164,10 @@ class GrantTypeBase(object):
raise errors.UnauthorizedClientError(request=request)
def validate_scopes(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
if not request.scopes:
request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list(
self.request_validator.get_default_scopes(request.client_id, request))
@@ -154,6 +182,13 @@ class GrantTypeBase(object):
Base classes can define a default response mode for their authorization
response by overriding the static `default_response_mode` member.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token:
+ :param headers:
+ :param body:
+ :param status:
"""
request.response_mode = request.response_mode or self.default_response_mode
@@ -183,3 +218,36 @@ class GrantTypeBase(object):
raise NotImplementedError(
'Subclasses must set a valid default_response_mode')
+
+ def _get_default_headers(self):
+ """Create default headers for grant responses."""
+ return {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ }
+
+ def _handle_redirects(self, request):
+ if request.redirect_uri is not None:
+ request.using_default_redirect_uri = False
+ log.debug('Using provided redirect_uri %s', request.redirect_uri)
+ if not is_absolute_uri(request.redirect_uri):
+ raise errors.InvalidRedirectURIError(request=request)
+
+ # The authorization server MUST verify that the redirection URI
+ # to which it will redirect the access token matches a
+ # redirection URI registered by the client as described in
+ # Section 3.1.2.
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
+ if not self.request_validator.validate_redirect_uri(
+ request.client_id, request.redirect_uri, request):
+ raise errors.MismatchingRedirectURIError(request=request)
+ else:
+ request.redirect_uri = self.request_validator.get_default_redirect_uri(
+ request.client_id, request)
+ request.using_default_redirect_uri = True
+ log.debug('Using default redirect_uri %s.', request.redirect_uri)
+ if not request.redirect_uri:
+ raise errors.MissingRedirectURIError(request=request)
+ if not is_absolute_uri(request.redirect_uri):
+ raise errors.InvalidRedirectURIError(request=request)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
index bf6c87f..c966795 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
@@ -47,31 +47,33 @@ class ClientCredentialsGrant(GrantTypeBase):
(B) The authorization server authenticates the client, and if valid,
issues an access token.
- .. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4
+ .. _`Client Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.4
"""
def create_token_response(self, request, token_handler):
"""Return token or error in JSON format.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
If the access token request is valid and authorized, the
authorization server issues an access token as described in
`Section 5.1`_. A refresh token SHOULD NOT be included. If the request
failed client authentication or is invalid, the authorization server
returns an error response as described in `Section 5.2`_.
- .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
- .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
"""
- headers = {
- 'Content-Type': 'application/json',
- 'Cache-Control': 'no-store',
- 'Pragma': 'no-cache',
- }
+ headers = self._get_default_headers()
try:
log.debug('Validating access token request, %r.', request)
self.validate_token_request(request)
except errors.OAuth2Error as e:
log.debug('Client error in token request. %s.', e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, refresh_token=False, save_token=False)
@@ -85,6 +87,10 @@ class ClientCredentialsGrant(GrantTypeBase):
return headers, json.dumps(token), 200
def validate_token_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
for validator in self.custom_validators.pre_token:
validator(request)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py
index 2b9c49d..d6de906 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/implicit.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/implicit.py
@@ -8,7 +8,6 @@ from __future__ import absolute_import, unicode_literals
import logging
from oauthlib import common
-from oauthlib.uri_validate import is_absolute_uri
from .. import errors
from .base import GrantTypeBase
@@ -111,9 +110,9 @@ class ImplicitGrant(GrantTypeBase):
See `Section 10.3`_ and `Section 10.16`_ for important security considerations
when using the implicit grant.
- .. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2
- .. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3
- .. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16
+ .. _`Implicit Grant`: https://tools.ietf.org/html/rfc6749#section-4.2
+ .. _`Section 10.3`: https://tools.ietf.org/html/rfc6749#section-10.3
+ .. _`Section 10.16`: https://tools.ietf.org/html/rfc6749#section-10.16
"""
response_types = ['token']
@@ -121,6 +120,12 @@ class ImplicitGrant(GrantTypeBase):
def create_authorization_response(self, request, token_handler):
"""Create an authorization response.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
The client constructs the request URI by adding the following
parameters to the query component of the authorization endpoint URI
using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
@@ -152,17 +157,22 @@ class ImplicitGrant(GrantTypeBase):
access token matches a redirection URI registered by the client as
described in `Section 3.1.2`_.
- .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
- .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
"""
return self.create_token_response(request, token_handler)
def create_token_response(self, request, token_handler):
"""Return token or error embedded in the URI fragment.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
If the resource owner grants the access request, the authorization
server issues an access token and delivers it to the client by adding
the following parameters to the fragment component of the redirection
@@ -195,16 +205,11 @@ class ImplicitGrant(GrantTypeBase):
The authorization server MUST NOT issue a refresh token.
- .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
"""
try:
- # request.scopes is only mandated in post auth and both pre and
- # post auth use validate_authorization_request
- if not request.scopes:
- raise ValueError('Scopes must be set on post auth.')
-
self.validate_token_request(request)
# If the request fails due to a missing, invalid, or mismatching
@@ -222,7 +227,7 @@ class ImplicitGrant(GrantTypeBase):
# the authorization server informs the client by adding the following
# parameters to the fragment component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B:
- # http://tools.ietf.org/html/rfc6749#appendix-B
+ # https://tools.ietf.org/html/rfc6749#appendix-B
except errors.OAuth2Error as e:
log.debug('Client error during validation of %r. %r.', request, e)
return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples,
@@ -248,11 +253,18 @@ class ImplicitGrant(GrantTypeBase):
request, token, {}, None, 302)
def validate_authorization_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
return self.validate_token_request(request)
def validate_token_request(self, request):
"""Check the token request for normal and fatal errors.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
This method is very similar to validate_authorization_request in
the AuthorizationCodeGrant but differ in a few subtle areas.
@@ -285,7 +297,7 @@ class ImplicitGrant(GrantTypeBase):
raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
# REQUIRED. The client identifier as described in Section 2.2.
- # http://tools.ietf.org/html/rfc6749#section-2.2
+ # https://tools.ietf.org/html/rfc6749#section-2.2
if not request.client_id:
raise errors.MissingClientIdError(request=request)
@@ -293,30 +305,8 @@ class ImplicitGrant(GrantTypeBase):
raise errors.InvalidClientIdError(request=request)
# OPTIONAL. As described in Section 3.1.2.
- # http://tools.ietf.org/html/rfc6749#section-3.1.2
- if request.redirect_uri is not None:
- request.using_default_redirect_uri = False
- log.debug('Using provided redirect_uri %s', request.redirect_uri)
- if not is_absolute_uri(request.redirect_uri):
- raise errors.InvalidRedirectURIError(request=request)
-
- # The authorization server MUST verify that the redirection URI
- # to which it will redirect the access token matches a
- # redirection URI registered by the client as described in
- # Section 3.1.2.
- # http://tools.ietf.org/html/rfc6749#section-3.1.2
- if not self.request_validator.validate_redirect_uri(
- request.client_id, request.redirect_uri, request):
- raise errors.MismatchingRedirectURIError(request=request)
- else:
- request.redirect_uri = self.request_validator.get_default_redirect_uri(
- request.client_id, request)
- request.using_default_redirect_uri = True
- log.debug('Using default redirect_uri %s.', request.redirect_uri)
- if not request.redirect_uri:
- raise errors.MissingRedirectURIError(request=request)
- if not is_absolute_uri(request.redirect_uri):
- raise errors.InvalidRedirectURIError(request=request)
+ # https://tools.ietf.org/html/rfc6749#section-3.1.2
+ self._handle_redirects(request)
# Then check for normal errors.
@@ -328,7 +318,7 @@ class ImplicitGrant(GrantTypeBase):
# the authorization server informs the client by adding the following
# parameters to the fragment component of the redirection URI using the
# "application/x-www-form-urlencoded" format, per Appendix B.
- # http://tools.ietf.org/html/rfc6749#appendix-B
+ # https://tools.ietf.org/html/rfc6749#appendix-B
# Note that the correct parameters to be added are automatically
# populated through the use of specific exceptions
@@ -351,7 +341,7 @@ class ImplicitGrant(GrantTypeBase):
raise errors.UnauthorizedClientError(request=request)
# OPTIONAL. The scope of the access request as described by Section 3.3
- # http://tools.ietf.org/html/rfc6749#section-3.3
+ # https://tools.ietf.org/html/rfc6749#section-3.3
self.validate_scopes(request)
request_info.update({
diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
index 6233e7c..bd519e8 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
@@ -19,7 +19,7 @@ class RefreshTokenGrant(GrantTypeBase):
"""`Refresh token grant`_
- .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6
+ .. _`Refresh token grant`: https://tools.ietf.org/html/rfc6749#section-6
"""
def __init__(self, request_validator=None,
@@ -33,6 +33,11 @@ class RefreshTokenGrant(GrantTypeBase):
def create_token_response(self, request, token_handler):
"""Create a new access token from a refresh_token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
If valid and authorized, the authorization server issues an access
token as described in `Section 5.1`_. If the request failed
verification or is invalid, the authorization server returns an error
@@ -46,18 +51,16 @@ class RefreshTokenGrant(GrantTypeBase):
identical to that of the refresh token included by the client in the
request.
- .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
- .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
"""
- headers = {
- 'Content-Type': 'application/json',
- 'Cache-Control': 'no-store',
- 'Pragma': 'no-cache',
- }
+ headers = self._get_default_headers()
try:
log.debug('Validating refresh token request, %r.', request)
self.validate_token_request(request)
except errors.OAuth2Error as e:
+ log.debug('Client error in token request, %s.', e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request,
@@ -72,6 +75,10 @@ class RefreshTokenGrant(GrantTypeBase):
return headers, json.dumps(token), 200
def validate_token_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
# REQUIRED. Value MUST be set to "refresh_token".
if request.grant_type != 'refresh_token':
raise errors.UnsupportedGrantTypeError(request=request)
@@ -90,7 +97,7 @@ class RefreshTokenGrant(GrantTypeBase):
# the client was issued client credentials (or assigned other
# authentication requirements), the client MUST authenticate with the
# authorization server as described in Section 3.2.1.
- # http://tools.ietf.org/html/rfc6749#section-3.2.1
+ # https://tools.ietf.org/html/rfc6749#section-3.2.1
if self.request_validator.client_authentication_required(request):
log.debug('Authenticating client, %r.', request)
if not self.request_validator.authenticate_client(request):
diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
index ede779a..f765d91 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
+++ b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
@@ -67,26 +67,27 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
the resource owner credentials, and if valid, issues an access
token.
- .. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3
+ .. _`Resource Owner Password Credentials Grant`: https://tools.ietf.org/html/rfc6749#section-4.3
"""
def create_token_response(self, request, token_handler):
"""Return token or error in json format.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param token_handler: A token handler instance, for example of type
+ oauthlib.oauth2.BearerToken.
+
If the access token request is valid and authorized, the
authorization server issues an access token and optional refresh
token as described in `Section 5.1`_. If the request failed client
authentication or is invalid, the authorization server returns an
error response as described in `Section 5.2`_.
- .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
- .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+ .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1
+ .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2
"""
- headers = {
- 'Content-Type': 'application/json',
- 'Cache-Control': 'no-store',
- 'Pragma': 'no-cache',
- }
+ headers = self._get_default_headers()
try:
if self.request_validator.client_authentication_required(request):
log.debug('Authenticating client, %r.', request)
@@ -100,6 +101,7 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
self.validate_token_request(request)
except errors.OAuth2Error as e:
log.debug('Client error in token request, %s.', e)
+ headers.update(e.headers)
return headers, e.json, e.status_code
token = token_handler.create_token(request, self.refresh_token, save_token=False)
@@ -114,6 +116,9 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
def validate_token_request(self, request):
"""
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
The client makes a request to the token endpoint by adding the
following parameters using the "application/x-www-form-urlencoded"
format per Appendix B with a character encoding of UTF-8 in the HTTP
@@ -153,8 +158,8 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
brute force attacks (e.g., using rate-limitation or generating
alerts).
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1
"""
for validator in self.custom_validators.pre_token:
validator(request)
diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py
index b87b146..4d0baee 100644
--- a/oauthlib/oauth2/rfc6749/parameters.py
+++ b/oauthlib/oauth2/rfc6749/parameters.py
@@ -5,7 +5,7 @@ oauthlib.oauth2.rfc6749.parameters
This module contains methods related to `Section 4`_ of the OAuth 2 RFC.
-.. _`Section 4`: http://tools.ietf.org/html/rfc6749#section-4
+.. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4
"""
from __future__ import absolute_import, unicode_literals
@@ -37,14 +37,14 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
using the ``application/x-www-form-urlencoded`` format as defined by
[`W3C.REC-html401-19991224`_]:
+ :param uri:
+ :param client_id: The client identifier as described in `Section 2.2`_.
:param response_type: To indicate which OAuth 2 grant/flow is required,
"code" and "token".
- :param client_id: The client identifier as described in `Section 2.2`_.
:param redirect_uri: The client provided URI to redirect back to after
authorization as described in `Section 3.1.2`_.
:param scope: The scope of the access request as described by
`Section 3.3`_.
-
:param state: An opaque value used by the client to maintain
state between the request and callback. The authorization
server includes this value when redirecting the user-agent
@@ -61,11 +61,11 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com
- .. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
- .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
- .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+ .. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
+ .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
+ .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
"""
if not is_secure_transport(uri):
raise InsecureTransportError()
@@ -87,7 +87,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
return add_params_to_uri(uri, params)
-def prepare_token_request(grant_type, body='', **kwargs):
+def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs):
"""Prepare the access token request.
The client makes a request to the token endpoint by adding the
@@ -95,15 +95,39 @@ def prepare_token_request(grant_type, body='', **kwargs):
format in the HTTP request entity-body:
:param grant_type: To indicate grant type being used, i.e. "password",
- "authorization_code" or "client_credentials".
- :param body: Existing request body to embed parameters in.
- :param code: If using authorization code grant, pass the previously
- obtained authorization code as the ``code`` argument.
+ "authorization_code" or "client_credentials".
+
+ :param body: Existing request body (URL encoded string) to embed parameters
+ into. This may contain extra paramters. Default ''.
+
+ :param include_client_id: `True` (default) to send the `client_id` in the
+ body of the upstream request. This is required
+ if the client is not authenticating with the
+ authorization server as described in
+ `Section 3.2.1`_.
+ :type include_client_id: Boolean
+
+ :param client_id: Unicode client identifier. Will only appear if
+ `include_client_id` is True. *
+
+ :param client_secret: Unicode client secret. Will only appear if set to a
+ value that is not `None`. Invoking this function with
+ an empty string will send an empty `client_secret`
+ value to the server. *
+
+ :param code: If using authorization_code grant, pass the previously
+ obtained authorization code as the ``code`` argument. *
+
:param redirect_uri: If the "redirect_uri" parameter was included in the
authorization request as described in
- `Section 4.1.1`_, and their values MUST be identical.
+ `Section 4.1.1`_, and their values MUST be identical. *
+
:param kwargs: Extra arguments to embed in the request body.
+ Parameters marked with a `*` above are not explicit arguments in the
+ function signature, but are specially documented arguments for items
+ appearing in the generic `**kwargs` keyworded input.
+
An example of an authorization code token request body:
.. code-block:: http
@@ -111,13 +135,26 @@ def prepare_token_request(grant_type, body='', **kwargs):
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
- .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
+ .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
"""
params = [('grant_type', grant_type)]
if 'scope' in kwargs:
kwargs['scope'] = list_to_scope(kwargs['scope'])
+ # pull the `client_id` out of the kwargs.
+ client_id = kwargs.pop('client_id', None)
+ if include_client_id:
+ if client_id is not None:
+ params.append((unicode_type('client_id'), client_id))
+
+ # the kwargs iteration below only supports including boolean truth (truthy)
+ # values, but some servers may require an empty string for `client_secret`
+ client_secret = kwargs.pop('client_secret', None)
+ if client_secret is not None:
+ params.append((unicode_type('client_secret'), client_secret))
+
+ # this handles: `code`, `redirect_uri`, and other undocumented params
for k in kwargs:
if kwargs[k]:
params.append((unicode_type(k), kwargs[k]))
@@ -133,15 +170,19 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token",
using the "application/x-www-form-urlencoded" format in the HTTP request
entity-body:
- token REQUIRED. The token that the client wants to get revoked.
+ :param token: REQUIRED. The token that the client wants to get revoked.
- token_type_hint OPTIONAL. A hint about the type of the token submitted
- for revocation. Clients MAY pass this parameter in order to help the
- authorization server to optimize the token lookup. If the server is unable
- to locate the token using the given hint, it MUST extend its search across
- all of its supported token types. An authorization server MAY ignore this
- parameter, particularly if it is able to detect the token type
- automatically. This specification defines two such values:
+ :param token_type_hint: OPTIONAL. A hint about the type of the token
+ submitted for revocation. Clients MAY pass this
+ parameter in order to help the authorization server
+ to optimize the token lookup. If the server is
+ unable to locate the token using the given hint, it
+ MUST extend its search across all of its supported
+ token types. An authorization server MAY ignore
+ this parameter, particularly if it is able to detect
+ the token type automatically.
+
+ This specification defines two values for `token_type_hint`:
* access_token: An access token as defined in [RFC6749],
`Section 1.4`_
@@ -153,9 +194,9 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token",
specification MAY define other values for this parameter using the
registry defined in `Section 4.1.2`_.
- .. _`Section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
- .. _`Section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
- .. _`Section 4.1.2`: http://tools.ietf.org/html/rfc7009#section-4.1.2
+ .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
+ .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
+ .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2
"""
if not is_secure_transport(url):
@@ -264,6 +305,10 @@ def parse_implicit_response(uri, state=None, scope=None):
authorization request. The exact value received from the
client.
+ :param uri:
+ :param state:
+ :param scope:
+
Similar to the authorization code response, but with a full token provided
in the URL fragment:
@@ -279,6 +324,10 @@ def parse_implicit_response(uri, state=None, scope=None):
fragment = urlparse.urlparse(uri).fragment
params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
+ for key in ('expires_in',):
+ if key in params: # cast things to int
+ params[key] = int(params[key])
+
if 'scope' in params:
params['scope'] = scope_to_list(params['scope'])
@@ -348,10 +397,10 @@ def parse_token_response(body, scope=None):
"example_parameter":"example_value"
}
- .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
- .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
- .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
- .. _`RFC4627`: http://tools.ietf.org/html/rfc4627
+ .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
+ .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
+ .. _`RFC4627`: https://tools.ietf.org/html/rfc4627
"""
try:
params = json.loads(body)
@@ -359,19 +408,16 @@ def parse_token_response(body, scope=None):
# Fall back to URL-encoded string, to support old implementations,
# including (at time of writing) Facebook. See:
- # https://github.com/idan/oauthlib/issues/267
+ # https://github.com/oauthlib/oauthlib/issues/267
params = dict(urlparse.parse_qsl(body))
- for key in ('expires_in', 'expires'):
- if key in params: # cast a couple things to int
+ for key in ('expires_in',):
+ if key in params: # cast things to int
params[key] = int(params[key])
if 'scope' in params:
params['scope'] = scope_to_list(params['scope'])
- if 'expires' in params:
- params['expires_in'] = params.pop('expires')
-
if 'expires_in' in params:
params['expires_at'] = time.time() + int(params['expires_in'])
@@ -395,7 +441,7 @@ def validate_token_parameters(params):
# If the issued access token scope is different from the one requested by
# the client, the authorization server MUST include the "scope" response
# parameter to inform the client of the actual scope granted.
- # http://tools.ietf.org/html/rfc6749#section-3.3
+ # https://tools.ietf.org/html/rfc6749#section-3.3
if params.scope_changed:
message = 'Scope has changed from "{old}" to "{new}".'.format(
old=params.old_scope, new=params.scope,
diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py
index ba129d5..193a9e1 100644
--- a/oauthlib/oauth2/rfc6749/request_validator.py
+++ b/oauthlib/oauth2/rfc6749/request_validator.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
-oauthlib.oauth2.rfc6749.grant_types
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+oauthlib.oauth2.rfc6749.request_validator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from __future__ import absolute_import, unicode_literals
@@ -26,7 +26,8 @@ class RequestValidator(object):
client credentials or whenever Client provided client authentication, see
`Section 6`_
- :param request: oauthlib.common.Request
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -34,9 +35,9 @@ class RequestValidator(object):
- Resource Owner Password Credentials Grant
- Refresh Token Grant
- .. _`Section 4.3.2`: http://tools.ietf.org/html/rfc6749#section-4.3.2
- .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3
- .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
+ .. _`Section 4.3.2`: https://tools.ietf.org/html/rfc6749#section-4.3.2
+ .. _`Section 4.1.3`: https://tools.ietf.org/html/rfc6749#section-4.1.3
+ .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
"""
return True
@@ -51,7 +52,8 @@ class RequestValidator(object):
both body and query can be obtained by direct attribute access, i.e.
request.client_id for client_id in the URL query.
- :param request: oauthlib.common.Request
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -60,7 +62,7 @@ class RequestValidator(object):
- Client Credentials Grant
- Refresh Token Grant
- .. _`HTTP Basic Authentication Scheme`: http://tools.ietf.org/html/rfc1945#section-11.1
+ .. _`HTTP Basic Authentication Scheme`: https://tools.ietf.org/html/rfc1945#section-11.1
"""
raise NotImplementedError('Subclasses must implement this method.')
@@ -74,7 +76,9 @@ class RequestValidator(object):
to set request.client to the client object associated with the
given client_id.
- :param request: oauthlib.common.Request
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -82,7 +86,7 @@ class RequestValidator(object):
"""
raise NotImplementedError('Subclasses must implement this method.')
- def confirm_redirect_uri(self, client_id, code, redirect_uri, client,
+ def confirm_redirect_uri(self, client_id, code, redirect_uri, client, request,
*args, **kwargs):
"""Ensure that the authorization process represented by this authorization
code began with this 'redirect_uri'.
@@ -93,11 +97,12 @@ class RequestValidator(object):
the client's allowed redirect URIs, but against the URI used when the
code was saved.
- :param client_id: Unicode client identifier
+ :param client_id: Unicode client identifier.
:param code: Unicode authorization_code.
- :param redirect_uri: Unicode absolute URI
- :param client: Client object set by you, see authenticate_client.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param redirect_uri: Unicode absolute URI.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -108,8 +113,9 @@ class RequestValidator(object):
def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
"""Get the default redirect URI for the client.
- :param client_id: Unicode client identifier
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: The default redirect URI for the client
Method is used by:
@@ -121,8 +127,9 @@ class RequestValidator(object):
def get_default_scopes(self, client_id, request, *args, **kwargs):
"""Get the default scopes for the client.
- :param client_id: Unicode client identifier
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: List of default scopes
Method is used by all core grant types:
@@ -136,8 +143,9 @@ class RequestValidator(object):
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
"""Get the list of scopes associated with the refresh token.
- :param refresh_token: Unicode refresh token
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param refresh_token: Unicode refresh token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: List of scopes.
Method is used by:
@@ -156,9 +164,10 @@ class RequestValidator(object):
used in situations where returning all valid scopes from the
get_original_scopes is not practical.
- :param request_scopes: A list of scopes that were requested by client
- :param refresh_token: Unicode refresh_token
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request_scopes: A list of scopes that were requested by client.
+ :param refresh_token: Unicode refresh_token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -166,12 +175,54 @@ class RequestValidator(object):
"""
return False
+ def introspect_token(self, token, token_type_hint, request, *args, **kwargs):
+ """Introspect an access or refresh token.
+
+ Called once the introspect request is validated. This method should
+ verify the *token* and either return a dictionary with the list of
+ claims associated, or `None` in case the token is unknown.
+
+ Below the list of registered claims you should be interested in:
+ - scope : space-separated list of scopes
+ - client_id : client identifier
+ - username : human-readable identifier for the resource owner
+ - token_type : type of the token
+ - exp : integer timestamp indicating when this token will expire
+ - iat : integer timestamp indicating when this token was issued
+ - nbf : integer timestamp indicating when it can be "not-before" used
+ - sub : subject of the token - identifier of the resource owner
+ - aud : list of string identifiers representing the intended audience
+ - iss : string representing issuer of this token
+ - jti : string identifier for the token
+
+ Note that most of them are coming directly from JWT RFC. More details
+ can be found in `Introspect Claims`_ or `_JWT Claims`_.
+
+ The implementation can use *token_type_hint* to improve lookup
+ efficency, but must fallback to other types to be compliant with RFC.
+
+ The dict of claims is added to request.token after this method.
+
+ :param token: The token string.
+ :param token_type_hint: access_token or refresh_token.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+
+ Method is used by:
+ - Introspect Endpoint (all grants are compatible)
+
+ .. _`Introspect Claims`: https://tools.ietf.org/html/rfc7662#section-2.2
+ .. _`JWT Claims`: https://tools.ietf.org/html/rfc7519#section-4
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
"""Invalidate an authorization code after use.
- :param client_id: Unicode client identifier
+ :param client_id: Unicode client identifier.
:param code: The authorization code grant (request.code).
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
Method is used by:
- Authorization Code Grant
@@ -183,7 +234,8 @@ class RequestValidator(object):
:param token: The token string.
:param token_type_hint: access_token or refresh_token.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
Method is used by:
- Revocation Endpoint
@@ -197,7 +249,8 @@ class RequestValidator(object):
or replaced with a new one (rotated). Return True to rotate and
and False for keeping original.
- :param request: oauthlib.common.Request
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -209,29 +262,34 @@ class RequestValidator(object):
"""Persist the authorization_code.
The code should at minimum be stored with:
- - the client_id (client_id)
- - the redirect URI used (request.redirect_uri)
- - a resource owner / user (request.user)
- - the authorized scopes (request.scopes)
- - the client state, if given (code.get('state'))
+ - the client_id (``client_id``)
+ - the redirect URI used (``request.redirect_uri``)
+ - a resource owner / user (``request.user``)
+ - the authorized scopes (``request.scopes``)
+ - the client state, if given (``code.get('state')``)
- The 'code' argument is actually a dictionary, containing at least a
- 'code' key with the actual authorization code:
+ To support PKCE, you MUST associate the code with:
+ - Code Challenge (``request.code_challenge``) and
+ - Code Challenge Method (``request.code_challenge_method``)
- {'code': 'sdf345jsdf0934f'}
+ The ``code`` argument is actually a dictionary, containing at least a
+ ``code`` key with the actual authorization code:
- It may also have a 'state' key containing a nonce for the client, if it
+ ``{'code': 'sdf345jsdf0934f'}``
+
+ It may also have a ``state`` key containing a nonce for the client, if it
chose to send one. That value should be saved and used in
- 'validate_code'.
+ ``.validate_code``.
- It may also have a 'claims' parameter which, when present, will be a dict
+ It may also have a ``claims`` parameter which, when present, will be a dict
deserialized from JSON as described at
http://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
- This value should be saved in this method and used again in 'validate_code'.
+ This value should be saved in this method and used again in ``.validate_code``.
- :param client_id: Unicode client identifier
+ :param client_id: Unicode client identifier.
:param code: A dict of the authorization code grant and, optionally, state.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
Method is used by:
- Authorization Code Grant
@@ -252,10 +310,12 @@ class RequestValidator(object):
blank value `""` don't forget to check it before using those values
in a select query if a database is used.
- :param client_id: Unicode client identifier
- :param code: Unicode authorization code grant
- :param redirect_uri: Unicode absolute URI
- :return: A list of scope
+ :param client_id: Unicode client identifier.
+ :param code: Unicode authorization code grant.
+ :param redirect_uri: Unicode absolute URI.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: A list of scopes
Method is used by:
- Authorization Token Grant Dispatcher
@@ -266,6 +326,10 @@ class RequestValidator(object):
"""Persist the token with a token type specific method.
Currently, only save_bearer_token is supported.
+
+ :param token: A (Bearer) token dict.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
"""
return self.save_bearer_token(token, request, *args, **kwargs)
@@ -292,16 +356,23 @@ class RequestValidator(object):
}
Note that while "scope" is a string-separated list of authorized scopes,
- the original list is still available in request.scopes
+ the original list is still available in request.scopes.
+
+ The token dict is passed as a reference so any changes made to the dictionary
+ will go back to the user. If additional information must return to the client
+ user, and it is only possible to get this information after writing the token
+ to storage, it should be added to the token dictionary. If the token
+ dictionary must be modified but the changes should not go back to the user,
+ a copy of the dictionary must be made before making the changes.
Also note that if an Authorization Code grant request included a valid claims
parameter (for OpenID Connect) then the request.claims property will contain
the claims dict, which should be saved for later use when generating the
id_token and/or UserInfo response content.
- :param client_id: Unicode client identifier
- :param token: A Bearer token dict
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param token: A Bearer token dict.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: The default redirect URI for the client
Method is used by all core grant types issuing Bearer tokens:
@@ -312,8 +383,25 @@ class RequestValidator(object):
"""
raise NotImplementedError('Subclasses must implement this method.')
- def get_id_token(self, token, token_handler, request):
+ def get_jwt_bearer_token(self, token, token_handler, request):
+ """Get JWT Bearer token or OpenID Connect ID token
+
+ If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token`
+
+ :param token: A Bearer token dict.
+ :param token_handler: The token handler (BearerToken class).
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT)
+
+ Method is used by JWT Bearer and OpenID Connect tokens:
+ - JWTToken.create_token
"""
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_id_token(self, token, token_handler, request):
+ """Get OpenID Connect ID token
+
In the OpenID Connect workflows when an ID Token is requested this method is called.
Subclasses should implement the construction, signing and optional encryption of the
ID Token as described in the OpenID Connect spec.
@@ -336,20 +424,70 @@ class RequestValidator(object):
.. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
.. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
- :param token: A Bearer token dict
- :param token_handler: the token handler (BearerToken class)
- :param request: the HTTP Request (oauthlib.common.Request)
+ :param token: A Bearer token dict.
+ :param token_handler: The token handler (BearerToken class)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:return: The ID Token (a JWS signed JWT)
"""
# the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token
raise NotImplementedError('Subclasses must implement this method.')
+ def validate_jwt_bearer_token(self, token, scopes, request):
+ """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes.
+
+ If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token`
+
+ If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response.
+
+ OpenID connect core 1.0 describe how to validate an id_token:
+ - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
+
+ :param token: Unicode Bearer token.
+ :param scopes: List of scopes (defined by you).
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is indirectly used by all core OpenID connect JWT token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Hybrid Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_id_token(self, token, scopes, request):
+ """Ensure the id token is valid and authorized access to scopes.
+
+ OpenID connect core 1.0 describe how to validate an id_token:
+ - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
+
+ :param token: Unicode Bearer token.
+ :param scopes: List of scopes (defined by you).
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is indirectly used by all core OpenID connect JWT token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Hybrid Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
def validate_bearer_token(self, token, scopes, request):
"""Ensure the Bearer token is valid and authorized access to scopes.
:param token: A string of random characters.
:param scopes: A list of scopes associated with the protected resource.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
A key to OAuth 2 security and restricting impact of leaked tokens is
the short expiration time of tokens, *always ensure the token has not
@@ -383,7 +521,8 @@ class RequestValidator(object):
:param token: Unicode Bearer token
:param scopes: List of scopes (defined by you)
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is indirectly used by all core Bearer token issuing grant types:
@@ -401,7 +540,9 @@ class RequestValidator(object):
to set request.client to the client object associated with the
given client_id.
- :param request: oauthlib.common.Request
+ :param client_id: Unicode client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -427,10 +568,16 @@ class RequestValidator(object):
The request.claims property, if it was given, should assigned a dict.
- :param client_id: Unicode client identifier
- :param code: Unicode authorization code
- :param client: Client object set by you, see authenticate_client.
- :param request: The HTTP Request (oauthlib.common.Request)
+ If PKCE is enabled (see 'is_pkce_required' and 'save_authorization_code')
+ you MUST set the following based on the information stored:
+ - request.code_challenge
+ - request.code_challenge_method
+
+ :param client_id: Unicode client identifier.
+ :param code: Unicode authorization code.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -441,10 +588,11 @@ class RequestValidator(object):
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
"""Ensure client is authorized to use the grant_type requested.
- :param client_id: Unicode client identifier
+ :param client_id: Unicode client identifier.
:param grant_type: Unicode grant type, i.e. authorization_code, password.
- :param client: Client object set by you, see authenticate_client.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -461,9 +609,10 @@ class RequestValidator(object):
All clients should register the absolute URIs of all URIs they intend
to redirect to. The registration is outside of the scope of oauthlib.
- :param client_id: Unicode client identifier
- :param redirect_uri: Unicode absolute URI
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param client_id: Unicode client identifier.
+ :param redirect_uri: Unicode absolute URI.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -478,9 +627,10 @@ class RequestValidator(object):
OBS! The request.user attribute should be set to the resource owner
associated with this refresh token.
- :param refresh_token: Unicode refresh token
- :param client: Client object set by you, see authenticate_client.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param refresh_token: Unicode refresh token.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -493,10 +643,11 @@ class RequestValidator(object):
def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
"""Ensure client is authorized to use the response_type requested.
- :param client_id: Unicode client identifier
+ :param client_id: Unicode client identifier.
:param response_type: Unicode response type, i.e. code, token.
- :param client: Client object set by you, see authenticate_client.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -508,10 +659,11 @@ class RequestValidator(object):
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""Ensure the client is authorized access to requested scopes.
- :param client_id: Unicode client identifier
- :param scopes: List of scopes (defined by you)
- :param client: Client object set by you, see authenticate_client.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param client_id: Unicode client identifier.
+ :param scopes: List of scopes (defined by you).
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by all core grant types:
@@ -528,7 +680,8 @@ class RequestValidator(object):
Silent OpenID authorization allows access tokens and id tokens to be
granted to clients without any user prompt or interaction.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -548,7 +701,8 @@ class RequestValidator(object):
not selected which one to link to the token then this method should
raise an oauthlib.oauth2.AccountSelectionRequired error.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -566,10 +720,11 @@ class RequestValidator(object):
not set you will be unable to associate a token with a user in the
persistance method used (commonly, save_bearer_token).
- :param username: Unicode username
- :param password: Unicode password
- :param client: Client object set by you, see authenticate_client.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param username: Unicode username.
+ :param password: Unicode password.
+ :param client: Client object set by you, see ``.authenticate_client``.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -586,7 +741,8 @@ class RequestValidator(object):
:param id_token_hint: User identifier string.
:param scopes: List of OAuth 2 scopes and OpenID claims (strings).
:param claims: OpenID Connect claims dict.
- :param request: The HTTP Request (oauthlib.common.Request)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
:rtype: True or False
Method is used by:
@@ -595,3 +751,78 @@ class RequestValidator(object):
- OpenIDConnectHybrid
"""
raise NotImplementedError('Subclasses must implement this method.')
+
+ def is_pkce_required(self, client_id, request):
+ """Determine if current request requires PKCE. Default, False.
+ This is called for both "authorization" and "token" requests.
+
+ Override this method by ``return True`` to enable PKCE for everyone.
+ You might want to enable it only for public clients.
+ Note that PKCE can also be used in addition of a client authentication.
+
+ OAuth 2.0 public clients utilizing the Authorization Code Grant are
+ susceptible to the authorization code interception attack. This
+ specification describes the attack as well as a technique to mitigate
+ against the threat through the use of Proof Key for Code Exchange
+ (PKCE, pronounced "pixy"). See `RFC7636`_.
+
+ :param client_id: Client identifier.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - Authorization Code Grant
+
+ .. _`RFC7636`: https://tools.ietf.org/html/rfc7636
+ """
+ return False
+
+ def get_code_challenge(self, code, request):
+ """Is called for every "token" requests.
+
+ When the server issues the authorization code in the authorization
+ response, it MUST associate the ``code_challenge`` and
+ ``code_challenge_method`` values with the authorization code so it can
+ be verified later.
+
+ Typically, the ``code_challenge`` and ``code_challenge_method`` values
+ are stored in encrypted form in the ``code`` itself but could
+ alternatively be stored on the server associated with the code. The
+ server MUST NOT include the ``code_challenge`` value in client requests
+ in a form that other entities can extract.
+
+ Return the ``code_challenge`` associated to the code.
+ If ``None`` is returned, code is considered to not be associated to any
+ challenges.
+
+ :param code: Authorization code.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: code_challenge string
+
+ Method is used by:
+ - Authorization Code Grant - when PKCE is active
+
+ """
+ return None
+
+ def get_code_challenge_method(self, code, request):
+ """Is called during the "token" request processing, when a
+ ``code_verifier`` and a ``code_challenge`` has been provided.
+
+ See ``.get_code_challenge``.
+
+ Must return ``plain`` or ``S256``. You can return a custom value if you have
+ implemented your own ``AuthorizationCodeGrant`` class.
+
+ :param code: Authorization code.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: code_challenge_method string
+
+ Method is used by:
+ - Authorization Code Grant - when PKCE is active
+
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py
index e0ac431..d78df09 100644
--- a/oauthlib/oauth2/rfc6749/tokens.py
+++ b/oauthlib/oauth2/rfc6749/tokens.py
@@ -4,8 +4,8 @@ oauthlib.oauth2.rfc6749.tokens
This module contains methods for adding two types of access tokens to requests.
-- Bearer http://tools.ietf.org/html/rfc6750
-- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+- Bearer https://tools.ietf.org/html/rfc6750
+- MAC https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
"""
from __future__ import absolute_import, unicode_literals
@@ -24,8 +24,6 @@ except ImportError:
from urllib.parse import urlparse
-
-
class OAuth2Token(dict):
def __init__(self, params, old_scope=None):
@@ -95,13 +93,17 @@ def prepare_mac_header(token, uri, key, http_method,
nonce="1336363200:dj83hs9s",
mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
- .. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
- .. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
+ .. _`MAC Access Authentication`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+ .. _`extension algorithms`: https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
+ :param token:
:param uri: Request URI.
- :param headers: Request headers as a dictionary.
- :param http_method: HTTP Request method.
:param key: MAC given provided by token endpoint.
+ :param http_method: HTTP Request method.
+ :param nonce:
+ :param headers: Request headers as a dictionary.
+ :param body:
+ :param ext:
:param hash_algorithm: HMAC algorithm provided by token endpoint.
:param issue_time: Time when the MAC credentials were issued (datetime).
:param draft: MAC authentication specification version.
@@ -182,7 +184,10 @@ def prepare_bearer_uri(token, uri):
http://www.example.com/path?access_token=h480djs93hd8
- .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+ .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
+
+ :param token:
+ :param uri:
"""
return add_params_to_uri(uri, [(('access_token', token))])
@@ -193,7 +198,10 @@ def prepare_bearer_headers(token, headers=None):
Authorization: Bearer h480djs93hd8
- .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+ .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
+
+ :param token:
+ :param headers:
"""
headers = headers or {}
headers['Authorization'] = 'Bearer %s' % token
@@ -205,16 +213,27 @@ def prepare_bearer_body(token, body=''):
access_token=h480djs93hd8
- .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+ .. _`Bearer Token`: https://tools.ietf.org/html/rfc6750
+
+ :param token:
+ :param body:
"""
return add_params_to_qs(body, [(('access_token', token))])
def random_token_generator(request, refresh_token=False):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param refresh_token:
+ """
return common.generate_token()
def signed_token_generator(private_pem, **kwargs):
+ """
+ :param private_pem:
+ """
def signed_token_generator(request):
request.claims = kwargs
return common.generate_signed_token(private_pem, request)
@@ -222,15 +241,43 @@ def signed_token_generator(private_pem, **kwargs):
return signed_token_generator
+def get_token_from_header(request):
+ """
+ Helper function to extract a token from the request header.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: Return the token or None if the Authorization header is malformed.
+ """
+ token = None
+
+ if 'Authorization' in request.headers:
+ split_header = request.headers.get('Authorization').split()
+ if len(split_header) == 2 and split_header[0] == 'Bearer':
+ token = split_header[1]
+ else:
+ token = request.access_token
+
+ return token
+
+
class TokenBase(object):
def __call__(self, request, refresh_token=False):
raise NotImplementedError('Subclasses must implement this method.')
def validate_request(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
raise NotImplementedError('Subclasses must implement this method.')
def estimate_type(self, request):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
raise NotImplementedError('Subclasses must implement this method.')
@@ -250,7 +297,14 @@ class BearerToken(TokenBase):
self.expires_in = expires_in or 3600
def create_token(self, request, refresh_token=False, save_token=True):
- """Create a BearerToken, by default without refresh token."""
+ """
+ Create a BearerToken, by default without refresh token.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :param refresh_token:
+ :param save_token:
+ """
if callable(self.expires_in):
expires_in = self.expires_in(request)
@@ -288,16 +342,20 @@ class BearerToken(TokenBase):
return token
def validate_request(self, request):
- token = None
- if 'Authorization' in request.headers:
- token = request.headers.get('Authorization')[7:]
- else:
- token = request.access_token
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ token = get_token_from_header(request)
return self.request_validator.validate_bearer_token(
token, request.scopes, request)
def estimate_type(self, request):
- if request.headers.get('Authorization', '').startswith('Bearer'):
+ """
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ """
+ if request.headers.get('Authorization', '').split(' ')[0] == 'Bearer':
return 9
elif request.access_token is not None:
return 5
diff --git a/oauthlib/openid/__init__.py b/oauthlib/openid/__init__.py
new file mode 100644
index 0000000..03f0fa2
--- /dev/null
+++ b/oauthlib/openid/__init__.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.openid
+~~~~~~~~~~~~~~
+
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .connect.core.endpoints import Server
diff --git a/oauthlib/openid/connect/__init__.py b/oauthlib/openid/connect/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/oauthlib/openid/connect/__init__.py
diff --git a/oauthlib/openid/connect/core/__init__.py b/oauthlib/openid/connect/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/oauthlib/openid/connect/core/__init__.py
diff --git a/oauthlib/openid/connect/core/endpoints/__init__.py b/oauthlib/openid/connect/core/endpoints/__init__.py
new file mode 100644
index 0000000..719f883
--- /dev/null
+++ b/oauthlib/openid/connect/core/endpoints/__init__.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oopenid.core
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OpenID Connect
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .pre_configured import Server
diff --git a/oauthlib/openid/connect/core/endpoints/pre_configured.py b/oauthlib/openid/connect/core/endpoints/pre_configured.py
new file mode 100644
index 0000000..6367847
--- /dev/null
+++ b/oauthlib/openid/connect/core/endpoints/pre_configured.py
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.openid.connect.core.endpoints.pre_configured
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various endpoints needed
+for providing OpenID Connect servers.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from oauthlib.oauth2.rfc6749.endpoints import (
+ AuthorizationEndpoint,
+ IntrospectEndpoint,
+ ResourceEndpoint,
+ RevocationEndpoint,
+ TokenEndpoint
+)
+from oauthlib.oauth2.rfc6749.grant_types import (
+ AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
+ ImplicitGrant as OAuth2ImplicitGrant,
+ ClientCredentialsGrant,
+ RefreshTokenGrant,
+ ResourceOwnerPasswordCredentialsGrant
+)
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from ..grant_types import (
+ AuthorizationCodeGrant,
+ ImplicitGrant,
+ HybridGrant,
+)
+from ..grant_types.dispatchers import (
+ AuthorizationCodeGrantDispatcher,
+ ImplicitTokenGrantDispatcher,
+ AuthorizationTokenGrantDispatcher
+)
+from ..tokens import JWTToken
+
+
+class Server(AuthorizationEndpoint, IntrospectEndpoint, TokenEndpoint,
+ ResourceEndpoint, RevocationEndpoint):
+
+ """An all-in-one endpoint featuring all four major grant types."""
+
+ def __init__(self, request_validator, token_expires_in=None,
+ token_generator=None, refresh_token_generator=None,
+ *args, **kwargs):
+ """Construct a new all-grants-in-one server.
+
+ :param request_validator: An implementation of
+ oauthlib.oauth2.RequestValidator.
+ :param token_expires_in: An int or a function to generate a token
+ expiration offset (in seconds) given a
+ oauthlib.common.Request object.
+ :param token_generator: A function to generate a token from a request.
+ :param refresh_token_generator: A function to generate a token from a
+ request for the refresh token.
+ :param kwargs: Extra parameters to pass to authorization-,
+ token-, resource-, and revocation-endpoint constructors.
+ """
+ auth_grant = OAuth2AuthorizationCodeGrant(request_validator)
+ implicit_grant = OAuth2ImplicitGrant(request_validator)
+ password_grant = ResourceOwnerPasswordCredentialsGrant(
+ request_validator)
+ credentials_grant = ClientCredentialsGrant(request_validator)
+ refresh_grant = RefreshTokenGrant(request_validator)
+ openid_connect_auth = AuthorizationCodeGrant(request_validator)
+ openid_connect_implicit = ImplicitGrant(request_validator)
+ openid_connect_hybrid = HybridGrant(request_validator)
+
+ bearer = BearerToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+
+ jwt = JWTToken(request_validator, token_generator,
+ token_expires_in, refresh_token_generator)
+
+ auth_grant_choice = AuthorizationCodeGrantDispatcher(default_grant=auth_grant, oidc_grant=openid_connect_auth)
+ implicit_grant_choice = ImplicitTokenGrantDispatcher(default_grant=implicit_grant, oidc_grant=openid_connect_implicit)
+
+ # See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Combinations for valid combinations
+ # internally our AuthorizationEndpoint will ensure they can appear in any order for any valid combination
+ AuthorizationEndpoint.__init__(self, default_response_type='code',
+ response_types={
+ 'code': auth_grant_choice,
+ 'token': implicit_grant_choice,
+ 'id_token': openid_connect_implicit,
+ 'id_token token': openid_connect_implicit,
+ 'code token': openid_connect_hybrid,
+ 'code id_token': openid_connect_hybrid,
+ 'code id_token token': openid_connect_hybrid,
+ 'none': auth_grant
+ },
+ default_token_type=bearer)
+
+ token_grant_choice = AuthorizationTokenGrantDispatcher(request_validator, default_grant=auth_grant, oidc_grant=openid_connect_auth)
+
+ TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+ grant_types={
+ 'authorization_code': token_grant_choice,
+ 'password': password_grant,
+ 'client_credentials': credentials_grant,
+ 'refresh_token': refresh_grant,
+ },
+ default_token_type=bearer)
+ ResourceEndpoint.__init__(self, default_token='Bearer',
+ token_types={'Bearer': bearer, 'JWT': jwt})
+ RevocationEndpoint.__init__(self, request_validator)
+ IntrospectEndpoint.__init__(self, request_validator)
diff --git a/oauthlib/openid/connect/core/exceptions.py b/oauthlib/openid/connect/core/exceptions.py
new file mode 100644
index 0000000..8b08d21
--- /dev/null
+++ b/oauthlib/openid/connect/core/exceptions.py
@@ -0,0 +1,152 @@
+# coding=utf-8
+"""
+oauthlib.oauth2.rfc6749.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 2 clients and providers to represent the spec
+defined error responses for all four core grant types.
+"""
+from __future__ import unicode_literals
+
+from oauthlib.oauth2.rfc6749.errors import FatalClientError, OAuth2Error
+
+
+class FatalOpenIDClientError(FatalClientError):
+ pass
+
+
+class OpenIDClientError(OAuth2Error):
+ pass
+
+
+class InteractionRequired(OpenIDClientError):
+ """
+ The Authorization Server requires End-User interaction to proceed.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User interaction.
+ """
+ error = 'interaction_required'
+ status_code = 401
+
+
+class LoginRequired(OpenIDClientError):
+ """
+ The Authorization Server requires End-User authentication.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User authentication.
+ """
+ error = 'login_required'
+ status_code = 401
+
+
+class AccountSelectionRequired(OpenIDClientError):
+ """
+ The End-User is REQUIRED to select a session at the Authorization Server.
+
+ The End-User MAY be authenticated at the Authorization Server with
+ different associated accounts, but the End-User did not select a session.
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface to prompt for a session to
+ use.
+ """
+ error = 'account_selection_required'
+
+
+class ConsentRequired(OpenIDClientError):
+ """
+ The Authorization Server requires End-User consent.
+
+ This error MAY be returned when the prompt parameter value in the
+ Authentication Request is none, but the Authentication Request cannot be
+ completed without displaying a user interface for End-User consent.
+ """
+ error = 'consent_required'
+ status_code = 401
+
+
+class InvalidRequestURI(OpenIDClientError):
+ """
+ The request_uri in the Authorization Request returns an error or
+ contains invalid data.
+ """
+ error = 'invalid_request_uri'
+ description = 'The request_uri in the Authorization Request returns an ' \
+ 'error or contains invalid data.'
+
+
+class InvalidRequestObject(OpenIDClientError):
+ """
+ The request parameter contains an invalid Request Object.
+ """
+ error = 'invalid_request_object'
+ description = 'The request parameter contains an invalid Request Object.'
+
+
+class RequestNotSupported(OpenIDClientError):
+ """
+ The OP does not support use of the request parameter.
+ """
+ error = 'request_not_supported'
+ description = 'The request parameter is not supported.'
+
+
+class RequestURINotSupported(OpenIDClientError):
+ """
+ The OP does not support use of the request_uri parameter.
+ """
+ error = 'request_uri_not_supported'
+ description = 'The request_uri parameter is not supported.'
+
+
+class RegistrationNotSupported(OpenIDClientError):
+ """
+ The OP does not support use of the registration parameter.
+ """
+ error = 'registration_not_supported'
+ description = 'The registration parameter is not supported.'
+
+
+class InvalidTokenError(OAuth2Error):
+ """
+ The access token provided is expired, revoked, malformed, or
+ invalid for other reasons. The resource SHOULD respond with
+ the HTTP 401 (Unauthorized) status code. The client MAY
+ request a new access token and retry the protected resource
+ request.
+ """
+ error = 'invalid_token'
+ status_code = 401
+ description = ("The access token provided is expired, revoked, malformed, "
+ "or invalid for other reasons.")
+
+
+class InsufficientScopeError(OAuth2Error):
+ """
+ The request requires higher privileges than provided by the
+ access token. The resource server SHOULD respond with the HTTP
+ 403 (Forbidden) status code and MAY include the "scope"
+ attribute with the scope necessary to access the protected
+ resource.
+ """
+ error = 'insufficient_scope'
+ status_code = 403
+ description = ("The request requires higher privileges than provided by "
+ "the access token.")
+
+
+def raise_from_error(error, params=None):
+ import inspect
+ import sys
+ kwargs = {
+ 'description': params.get('error_description'),
+ 'uri': params.get('error_uri'),
+ 'state': params.get('state')
+ }
+ for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
+ if cls.error == error:
+ raise cls(**kwargs)
diff --git a/oauthlib/openid/connect/core/grant_types/__init__.py b/oauthlib/openid/connect/core/grant_types/__init__.py
new file mode 100644
index 0000000..63f30ac
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/__init__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+from .authorization_code import AuthorizationCodeGrant
+from .implicit import ImplicitGrant
+from .base import GrantTypeBase
+from .hybrid import HybridGrant
+from .exceptions import OIDCNoPrompt
+from .dispatchers import (
+ AuthorizationCodeGrantDispatcher,
+ ImplicitTokenGrantDispatcher,
+ AuthorizationTokenGrantDispatcher
+)
diff --git a/oauthlib/openid/connect/core/grant_types/authorization_code.py b/oauthlib/openid/connect/core/grant_types/authorization_code.py
new file mode 100644
index 0000000..b0b1015
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/authorization_code.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant
+
+from .base import GrantTypeBase
+
+log = logging.getLogger(__name__)
+
+
+class AuthorizationCodeGrant(GrantTypeBase):
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.proxy_target = OAuth2AuthorizationCodeGrant(
+ request_validator=request_validator, **kwargs)
+ self.custom_validators.post_auth.append(
+ self.openid_authorization_validator)
+ self.register_token_modifier(self.add_id_token)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py b/oauthlib/openid/connect/core/grant_types/base.py
index 4371b28..fa578a5 100644
--- a/oauthlib/oauth2/rfc6749/grant_types/openid_connect.py
+++ b/oauthlib/openid/connect/core/grant_types/base.py
@@ -1,141 +1,15 @@
-# -*- coding: utf-8 -*-
-"""
-oauthlib.oauth2.rfc6749.grant_types.openid_connect
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-"""
-from __future__ import absolute_import, unicode_literals
+from .exceptions import OIDCNoPrompt
import datetime
import logging
from json import loads
-from ..errors import ConsentRequired, InvalidRequestError, LoginRequired
-from ..request_validator import RequestValidator
-from .authorization_code import AuthorizationCodeGrant
-from .implicit import ImplicitGrant
+from oauthlib.oauth2.rfc6749.errors import ConsentRequired, InvalidRequestError, LoginRequired
log = logging.getLogger(__name__)
-class OIDCNoPrompt(Exception):
- """Exception used to inform users that no explicit authorization is needed.
-
- Normally users authorize requests after validation of the request is done.
- Then post-authorization validation is again made and a response containing
- an auth code or token is created. However, when OIDC clients request
- no prompting of user authorization the final response is created directly.
-
- Example (without the shortcut for no prompt)
-
- scopes, req_info = endpoint.validate_authorization_request(url, ...)
- authorization_view = create_fancy_auth_form(scopes, req_info)
- return authorization_view
-
- Example (with the no prompt shortcut)
- try:
- scopes, req_info = endpoint.validate_authorization_request(url, ...)
- authorization_view = create_fancy_auth_form(scopes, req_info)
- return authorization_view
- except OIDCNoPrompt:
- # Note: Location will be set for you
- headers, body, status = endpoint.create_authorization_response(url, ...)
- redirect_view = create_redirect(headers, body, status)
- return redirect_view
- """
-
- def __init__(self):
- msg = ("OIDC request for no user interaction received. Do not ask user "
- "for authorization, it should been done using silent "
- "authentication through create_authorization_response. "
- "See OIDCNoPrompt.__doc__ for more details.")
- super(OIDCNoPrompt, self).__init__(msg)
-
-
-class AuthCodeGrantDispatcher(object):
- """
- This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope
- including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested.
- """
- def __init__(self, default_auth_grant=None, oidc_auth_grant=None):
- self.default_auth_grant = default_auth_grant
- self.oidc_auth_grant = oidc_auth_grant
-
- def _handler_for_request(self, request):
- handler = self.default_auth_grant
-
- if request.scopes and "openid" in request.scopes:
- handler = self.oidc_auth_grant
-
- log.debug('Selecting handler for request %r.', handler)
- return handler
-
- def create_authorization_response(self, request, token_handler):
- return self._handler_for_request(request).create_authorization_response(request, token_handler)
-
- def validate_authorization_request(self, request):
- return self._handler_for_request(request).validate_authorization_request(request)
-
-
-class ImplicitTokenGrantDispatcher(object):
- """
- This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope
- including 'openid' to either the default_auth_grant or the oidc_auth_grant based on the scopes requested.
- """
- def __init__(self, default_implicit_grant=None, oidc_implicit_grant=None):
- self.default_implicit_grant = default_implicit_grant
- self.oidc_implicit_grant = oidc_implicit_grant
-
- def _handler_for_request(self, request):
- handler = self.default_implicit_grant
-
- if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type:
- handler = self.oidc_implicit_grant
-
- log.debug('Selecting handler for request %r.', handler)
- return handler
-
- def create_authorization_response(self, request, token_handler):
- return self._handler_for_request(request).create_authorization_response(request, token_handler)
-
- def validate_authorization_request(self, request):
- return self._handler_for_request(request).validate_authorization_request(request)
-
-
-class AuthTokenGrantDispatcher(object):
- """
- This is an adapter class that will route simple Token requests, those that authorization_code have a scope
- including 'openid' to either the default_token_grant or the oidc_token_grant based on the scopes requested.
- """
- def __init__(self, request_validator, default_token_grant=None, oidc_token_grant=None):
- self.default_token_grant = default_token_grant
- self.oidc_token_grant = oidc_token_grant
- self.request_validator = request_validator
-
- def _handler_for_request(self, request):
- handler = self.default_token_grant
- scopes = ()
- parameters = dict(request.decoded_body)
- client_id = parameters.get('client_id', None)
- code = parameters.get('code', None)
- redirect_uri = parameters.get('redirect_uri', None)
-
- # If code is not pressent fallback to `default_token_grant` wich will
- # raise an error for the missing `code` in `create_token_response` step.
- if code:
- scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request)
-
- if 'openid' in scopes:
- handler = self.oidc_token_grant
-
- log.debug('Selecting handler for request %r.', handler)
- return handler
-
- def create_token_response(self, request, token_handler):
- handler = self._handler_for_request(request)
- return handler.create_token_response(request, token_handler)
-
-
-class OpenIDConnectBase(object):
+class GrantTypeBase(object):
# Just proxy the majority of method calls through to the
# proxy_target grant type handler, which will usually be either
@@ -351,12 +225,6 @@ class OpenIDConnectBase(object):
msg = "Prompt none is mutually exclusive with other values."
raise InvalidRequestError(request=request, description=msg)
- # prompt other than 'none' should be handled by the server code that
- # uses oauthlib
- if not request.id_token_hint:
- msg = "Prompt is set to none yet id_token_hint is missing."
- raise InvalidRequestError(request=request, description=msg)
-
if not self.request_validator.validate_silent_login(request):
raise LoginRequired(request=request)
@@ -406,46 +274,4 @@ class OpenIDConnectBase(object):
return {}
-class OpenIDConnectAuthCode(OpenIDConnectBase):
-
- def __init__(self, request_validator=None, **kwargs):
- self.proxy_target = AuthorizationCodeGrant(
- request_validator=request_validator, **kwargs)
- self.custom_validators.post_auth.append(
- self.openid_authorization_validator)
- self.register_token_modifier(self.add_id_token)
-
-
-class OpenIDConnectImplicit(OpenIDConnectBase):
-
- def __init__(self, request_validator=None, **kwargs):
- self.proxy_target = ImplicitGrant(
- request_validator=request_validator, **kwargs)
- self.register_response_type('id_token')
- self.register_response_type('id_token token')
- self.custom_validators.post_auth.append(
- self.openid_authorization_validator)
- self.custom_validators.post_auth.append(
- self.openid_implicit_authorization_validator)
- self.register_token_modifier(self.add_id_token)
-
-
-class OpenIDConnectHybrid(OpenIDConnectBase):
-
- def __init__(self, request_validator=None, **kwargs):
- self.request_validator = request_validator or RequestValidator()
-
- self.proxy_target = AuthorizationCodeGrant(
- request_validator=request_validator, **kwargs)
- # All hybrid response types should be fragment-encoded.
- self.proxy_target.default_response_mode = "fragment"
- self.register_response_type('code id_token')
- self.register_response_type('code token')
- self.register_response_type('code id_token token')
- self.custom_validators.post_auth.append(
- self.openid_authorization_validator)
- # Hybrid flows can return the id_token from the authorization
- # endpoint as part of the 'code' response
- self.register_code_modifier(self.add_token)
- self.register_code_modifier(self.add_id_token)
- self.register_token_modifier(self.add_id_token)
+OpenIDConnectBase = GrantTypeBase
diff --git a/oauthlib/openid/connect/core/grant_types/dispatchers.py b/oauthlib/openid/connect/core/grant_types/dispatchers.py
new file mode 100644
index 0000000..be8e2f3
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/dispatchers.py
@@ -0,0 +1,91 @@
+import logging
+log = logging.getLogger(__name__)
+
+
+class Dispatcher(object):
+ default_grant = None
+ oidc_grant = None
+
+
+class AuthorizationCodeGrantDispatcher(Dispatcher):
+ """
+ This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope
+ including 'openid' to either the default_grant or the oidc_grant based on the scopes requested.
+ """
+ def __init__(self, default_grant=None, oidc_grant=None):
+ self.default_grant = default_grant
+ self.oidc_grant = oidc_grant
+
+ def _handler_for_request(self, request):
+ handler = self.default_grant
+
+ if request.scopes and "openid" in request.scopes:
+ handler = self.oidc_grant
+
+ log.debug('Selecting handler for request %r.', handler)
+ return handler
+
+ def create_authorization_response(self, request, token_handler):
+ return self._handler_for_request(request).create_authorization_response(request, token_handler)
+
+ def validate_authorization_request(self, request):
+ return self._handler_for_request(request).validate_authorization_request(request)
+
+
+class ImplicitTokenGrantDispatcher(Dispatcher):
+ """
+ This is an adapter class that will route simple Authorization Code requests, those that have response_type=code and a scope
+ including 'openid' to either the default_grant or the oidc_grant based on the scopes requested.
+ """
+ def __init__(self, default_grant=None, oidc_grant=None):
+ self.default_grant = default_grant
+ self.oidc_grant = oidc_grant
+
+ def _handler_for_request(self, request):
+ handler = self.default_grant
+
+ if request.scopes and "openid" in request.scopes and 'id_token' in request.response_type:
+ handler = self.oidc_grant
+
+ log.debug('Selecting handler for request %r.', handler)
+ return handler
+
+ def create_authorization_response(self, request, token_handler):
+ return self._handler_for_request(request).create_authorization_response(request, token_handler)
+
+ def validate_authorization_request(self, request):
+ return self._handler_for_request(request).validate_authorization_request(request)
+
+
+class AuthorizationTokenGrantDispatcher(Dispatcher):
+ """
+ This is an adapter class that will route simple Token requests, those that authorization_code have a scope
+ including 'openid' to either the default_grant or the oidc_grant based on the scopes requested.
+ """
+ def __init__(self, request_validator, default_grant=None, oidc_grant=None):
+ self.default_grant = default_grant
+ self.oidc_grant = oidc_grant
+ self.request_validator = request_validator
+
+ def _handler_for_request(self, request):
+ handler = self.default_grant
+ scopes = ()
+ parameters = dict(request.decoded_body)
+ client_id = parameters.get('client_id', None)
+ code = parameters.get('code', None)
+ redirect_uri = parameters.get('redirect_uri', None)
+
+ # If code is not pressent fallback to `default_grant` wich will
+ # raise an error for the missing `code` in `create_token_response` step.
+ if code:
+ scopes = self.request_validator.get_authorization_code_scopes(client_id, code, redirect_uri, request)
+
+ if 'openid' in scopes:
+ handler = self.oidc_grant
+
+ log.debug('Selecting handler for request %r.', handler)
+ return handler
+
+ def create_token_response(self, request, token_handler):
+ handler = self._handler_for_request(request)
+ return handler.create_token_response(request, token_handler)
diff --git a/oauthlib/openid/connect/core/grant_types/exceptions.py b/oauthlib/openid/connect/core/grant_types/exceptions.py
new file mode 100644
index 0000000..809f1b3
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/exceptions.py
@@ -0,0 +1,32 @@
+class OIDCNoPrompt(Exception):
+ """Exception used to inform users that no explicit authorization is needed.
+
+ Normally users authorize requests after validation of the request is done.
+ Then post-authorization validation is again made and a response containing
+ an auth code or token is created. However, when OIDC clients request
+ no prompting of user authorization the final response is created directly.
+
+ Example (without the shortcut for no prompt)
+
+ scopes, req_info = endpoint.validate_authorization_request(url, ...)
+ authorization_view = create_fancy_auth_form(scopes, req_info)
+ return authorization_view
+
+ Example (with the no prompt shortcut)
+ try:
+ scopes, req_info = endpoint.validate_authorization_request(url, ...)
+ authorization_view = create_fancy_auth_form(scopes, req_info)
+ return authorization_view
+ except OIDCNoPrompt:
+ # Note: Location will be set for you
+ headers, body, status = endpoint.create_authorization_response(url, ...)
+ redirect_view = create_redirect(headers, body, status)
+ return redirect_view
+ """
+
+ def __init__(self):
+ msg = ("OIDC request for no user interaction received. Do not ask user "
+ "for authorization, it should been done using silent "
+ "authentication through create_authorization_response. "
+ "See OIDCNoPrompt.__doc__ for more details.")
+ super(OIDCNoPrompt, self).__init__(msg)
diff --git a/oauthlib/openid/connect/core/grant_types/hybrid.py b/oauthlib/openid/connect/core/grant_types/hybrid.py
new file mode 100644
index 0000000..54669ae
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/hybrid.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.oauth2.rfc6749.grant_types.authorization_code import AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant
+
+from .base import GrantTypeBase
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class HybridGrant(GrantTypeBase):
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.request_validator = request_validator or RequestValidator()
+
+ self.proxy_target = OAuth2AuthorizationCodeGrant(
+ request_validator=request_validator, **kwargs)
+ # All hybrid response types should be fragment-encoded.
+ self.proxy_target.default_response_mode = "fragment"
+ self.register_response_type('code id_token')
+ self.register_response_type('code token')
+ self.register_response_type('code id_token token')
+ self.custom_validators.post_auth.append(
+ self.openid_authorization_validator)
+ # Hybrid flows can return the id_token from the authorization
+ # endpoint as part of the 'code' response
+ self.register_code_modifier(self.add_token)
+ self.register_code_modifier(self.add_id_token)
+ self.register_token_modifier(self.add_id_token)
diff --git a/oauthlib/openid/connect/core/grant_types/implicit.py b/oauthlib/openid/connect/core/grant_types/implicit.py
new file mode 100644
index 0000000..0eaa5b3
--- /dev/null
+++ b/oauthlib/openid/connect/core/grant_types/implicit.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.openid.connect.core.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from .base import GrantTypeBase
+
+from oauthlib.oauth2.rfc6749.grant_types.implicit import ImplicitGrant as OAuth2ImplicitGrant
+
+log = logging.getLogger(__name__)
+
+
+class ImplicitGrant(GrantTypeBase):
+
+ def __init__(self, request_validator=None, **kwargs):
+ self.proxy_target = OAuth2ImplicitGrant(
+ request_validator=request_validator, **kwargs)
+ self.register_response_type('id_token')
+ self.register_response_type('id_token token')
+ self.custom_validators.post_auth.append(
+ self.openid_authorization_validator)
+ self.custom_validators.post_auth.append(
+ self.openid_implicit_authorization_validator)
+ self.register_token_modifier(self.add_id_token)
diff --git a/oauthlib/openid/connect/core/request_validator.py b/oauthlib/openid/connect/core/request_validator.py
new file mode 100644
index 0000000..1587754
--- /dev/null
+++ b/oauthlib/openid/connect/core/request_validator.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.openid.connect.core.request_validator
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.oauth2.rfc6749.request_validator import RequestValidator as OAuth2RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class RequestValidator(OAuth2RequestValidator):
+
+ def get_authorization_code_scopes(self, client_id, code, redirect_uri, request):
+ """ Extracts scopes from saved authorization code.
+
+ The scopes returned by this method is used to route token requests
+ based on scopes passed to Authorization Code requests.
+
+ With that the token endpoint knows when to include OpenIDConnect
+ id_token in token response only based on authorization code scopes.
+
+ Only code param should be sufficient to retrieve grant code from
+ any storage you are using, `client_id` and `redirect_uri` can gave a
+ blank value `""` don't forget to check it before using those values
+ in a select query if a database is used.
+
+ :param client_id: Unicode client identifier
+ :param code: Unicode authorization code grant
+ :param redirect_uri: Unicode absolute URI
+ :return: A list of scope
+
+ Method is used by:
+ - Authorization Token Grant Dispatcher
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_jwt_bearer_token(self, token, token_handler, request):
+ """Get JWT Bearer token or OpenID Connect ID token
+
+ If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token`
+
+ :param token: A Bearer token dict
+ :param token_handler: the token handler (BearerToken class)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: The JWT Bearer token or OpenID Connect ID token (a JWS signed JWT)
+
+ Method is used by JWT Bearer and OpenID Connect tokens:
+ - JWTToken.create_token
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def get_id_token(self, token, token_handler, request):
+ """Get OpenID Connect ID token
+
+ In the OpenID Connect workflows when an ID Token is requested this method is called.
+ Subclasses should implement the construction, signing and optional encryption of the
+ ID Token as described in the OpenID Connect spec.
+
+ In addition to the standard OAuth2 request properties, the request may also contain
+ these OIDC specific properties which are useful to this method:
+
+ - nonce, if workflow is implicit or hybrid and it was provided
+ - claims, if provided to the original Authorization Code request
+
+ The token parameter is a dict which may contain an ``access_token`` entry, in which
+ case the resulting ID Token *should* include a calculated ``at_hash`` claim.
+
+ Similarly, when the request parameter has a ``code`` property defined, the ID Token
+ *should* include a calculated ``c_hash`` claim.
+
+ http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_)
+
+ .. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
+ .. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
+ .. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
+
+ :param token: A Bearer token dict
+ :param token_handler: the token handler (BearerToken class)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :return: The ID Token (a JWS signed JWT)
+ """
+ # the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_jwt_bearer_token(self, token, scopes, request):
+ """Ensure the JWT Bearer token or OpenID Connect ID token are valids and authorized access to scopes.
+
+ If using OpenID Connect this SHOULD call `oauthlib.oauth2.RequestValidator.get_id_token`
+
+ If not using OpenID Connect this can `return None` to avoid 5xx rather 401/3 response.
+
+ OpenID connect core 1.0 describe how to validate an id_token:
+ - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
+
+ :param token: Unicode Bearer token
+ :param scopes: List of scopes (defined by you)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is indirectly used by all core OpenID connect JWT token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Hybrid Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_id_token(self, token, scopes, request):
+ """Ensure the id token is valid and authorized access to scopes.
+
+ OpenID connect core 1.0 describe how to validate an id_token:
+ - http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation
+ - http://openid.net/specs/openid-connect-core-1_0.html#HybridIDTValidation2
+
+ :param token: Unicode Bearer token
+ :param scopes: List of scopes (defined by you)
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is indirectly used by all core OpenID connect JWT token issuing grant types:
+ - Authorization Code Grant
+ - Implicit Grant
+ - Hybrid Grant
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_silent_authorization(self, request):
+ """Ensure the logged in user has authorized silent OpenID authorization.
+
+ Silent OpenID authorization allows access tokens and id tokens to be
+ granted to clients without any user prompt or interaction.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - OpenIDConnectAuthCode
+ - OpenIDConnectImplicit
+ - OpenIDConnectHybrid
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_silent_login(self, request):
+ """Ensure session user has authorized silent OpenID login.
+
+ If no user is logged in or has not authorized silent login, this
+ method should return False.
+
+ If the user is logged in but associated with multiple accounts and
+ not selected which one to link to the token then this method should
+ raise an oauthlib.oauth2.AccountSelectionRequired error.
+
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - OpenIDConnectAuthCode
+ - OpenIDConnectImplicit
+ - OpenIDConnectHybrid
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
+
+ def validate_user_match(self, id_token_hint, scopes, claims, request):
+ """Ensure client supplied user id hint matches session user.
+
+ If the sub claim or id_token_hint is supplied then the session
+ user must match the given ID.
+
+ :param id_token_hint: User identifier string.
+ :param scopes: List of OAuth 2 scopes and OpenID claims (strings).
+ :param claims: OpenID Connect claims dict.
+ :param request: OAuthlib request.
+ :type request: oauthlib.common.Request
+ :rtype: True or False
+
+ Method is used by:
+ - OpenIDConnectAuthCode
+ - OpenIDConnectImplicit
+ - OpenIDConnectHybrid
+ """
+ raise NotImplementedError('Subclasses must implement this method.')
diff --git a/oauthlib/openid/connect/core/tokens.py b/oauthlib/openid/connect/core/tokens.py
new file mode 100644
index 0000000..6b68891
--- /dev/null
+++ b/oauthlib/openid/connect/core/tokens.py
@@ -0,0 +1,54 @@
+"""
+authlib.openid.connect.core.tokens
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods for adding JWT tokens to requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+
+from oauthlib.oauth2.rfc6749.tokens import TokenBase, random_token_generator
+
+
+class JWTToken(TokenBase):
+ __slots__ = (
+ 'request_validator', 'token_generator',
+ 'refresh_token_generator', 'expires_in'
+ )
+
+ def __init__(self, request_validator=None, token_generator=None,
+ expires_in=None, refresh_token_generator=None):
+ self.request_validator = request_validator
+ self.token_generator = token_generator or random_token_generator
+ self.refresh_token_generator = (
+ refresh_token_generator or self.token_generator
+ )
+ self.expires_in = expires_in or 3600
+
+ def create_token(self, request, refresh_token=False, save_token=False):
+ """Create a JWT Token, using requestvalidator method."""
+
+ if callable(self.expires_in):
+ expires_in = self.expires_in(request)
+ else:
+ expires_in = self.expires_in
+
+ request.expires_in = expires_in
+
+ return self.request_validator.get_jwt_bearer_token(None, None, request)
+
+ def validate_request(self, request):
+ token = None
+ if 'Authorization' in request.headers:
+ token = request.headers.get('Authorization')[7:]
+ else:
+ token = request.access_token
+ return self.request_validator.validate_jwt_bearer_token(
+ token, request.scopes, request)
+
+ def estimate_type(self, request):
+ token = request.headers.get('Authorization', '')[7:]
+ if token.startswith('ey') and token.count('.') in (2, 4):
+ return 10
+ else:
+ return 0
diff --git a/oauthlib/signals.py b/oauthlib/signals.py
index 2f86650..22d47a4 100644
--- a/oauthlib/signals.py
+++ b/oauthlib/signals.py
@@ -8,7 +8,7 @@ signals_available = False
try:
from blinker import Namespace
signals_available = True
-except ImportError:
+except ImportError: # noqa
class Namespace(object):
def signal(self, name, doc=None):
return _FakeSignal(name, doc)
diff --git a/requirements-test.txt b/requirements-test.txt
index e761883..64485a6 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,4 +1,4 @@
-r requirements.txt
-coverage>=3.7.1
-nose==1.3.7
-mock==1.0.1
+mock>=2.0
+pytest>=4.0
+pytest-cov>=2.6
diff --git a/requirements.txt b/requirements.txt
index e4980c7..a4614bb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-pyjwt==1.0.0
-blinker==1.3
-cryptography>=0.8.1
+pyjwt==1.6.0
+blinker==1.4
+cryptography>=1.4.0
diff --git a/setup.cfg b/setup.cfg
index 2a9acf1..ed8a958 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,5 @@
[bdist_wheel]
universal = 1
+
+[metadata]
+license_file = LICENSE
diff --git a/setup.py b/setup.py
index 43f4d95..3f822e0 100755
--- a/setup.py
+++ b/setup.py
@@ -18,38 +18,30 @@ def fread(fn):
with open(join(dirname(__file__), fn), 'r') as f:
return f.read()
-if sys.version_info[0] == 3:
- tests_require = ['nose', 'cryptography', 'pyjwt>=1.0.0', 'blinker']
-else:
- tests_require = ['nose', 'unittest2', 'cryptography', 'mock', 'pyjwt>=1.0.0', 'blinker']
+
rsa_require = ['cryptography']
signedtoken_require = ['cryptography', 'pyjwt>=1.0.0']
signals_require = ['blinker']
-requires = []
-
setup(
name='oauthlib',
version=oauthlib.__version__,
description='A generic, spec-compliant, thorough implementation of the OAuth request-signing logic',
long_description=fread('README.rst'),
- author='Idan Gazit',
+ author='The OAuthlib Community',
author_email='idan@gazit.me',
maintainer='Ib Lundgren',
maintainer_email='ib.lundgren@gmail.com',
- url='https://github.com/idan/oauthlib',
+ url='https://github.com/oauthlib/oauthlib',
platforms='any',
license='BSD',
packages=find_packages(exclude=('docs', 'tests', 'tests.*')),
- test_suite='nose.collector',
- tests_require=tests_require,
+ python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
extras_require={
- 'test': tests_require,
'rsa': rsa_require,
'signedtoken': signedtoken_require,
'signals': signals_require,
},
- install_requires=requires,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
@@ -66,6 +58,7 @@ setup(
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: Implementation',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
diff --git a/tests/oauth1/rfc5849/endpoints/test_authorization.py b/tests/oauth1/rfc5849/endpoints/test_authorization.py
index 022e8e9..e9d3604 100644
--- a/tests/oauth1/rfc5849/endpoints/test_authorization.py
+++ b/tests/oauth1/rfc5849/endpoints/test_authorization.py
@@ -6,7 +6,7 @@ from oauthlib.oauth1 import RequestValidator
from oauthlib.oauth1.rfc5849 import errors
from oauthlib.oauth1.rfc5849.endpoints import AuthorizationEndpoint
-from ....unittest import TestCase
+from tests.unittest import TestCase
class AuthorizationEndpointTest(TestCase):
diff --git a/tests/oauth1/rfc5849/endpoints/test_base.py b/tests/oauth1/rfc5849/endpoints/test_base.py
index 8c41cf2..60f7860 100644
--- a/tests/oauth1/rfc5849/endpoints/test_base.py
+++ b/tests/oauth1/rfc5849/endpoints/test_base.py
@@ -60,7 +60,7 @@ class BaseEndpointTest(TestCase):
def test_expired_timestamp(self):
headers = {}
for pattern in ('12345678901', '4567890123', '123456789K'):
- headers['Authorization'] = sub('timestamp="\d*k?"',
+ headers['Authorization'] = sub(r'timestamp="\d*k?"',
'timestamp="%s"' % pattern,
self.headers['Authorization'])
h, b, s = self.endpoint.create_request_token_response(
diff --git a/tests/oauth1/rfc5849/test_client.py b/tests/oauth1/rfc5849/test_client.py
index dcb4c3d..e1f83de 100644
--- a/tests/oauth1/rfc5849/test_client.py
+++ b/tests/oauth1/rfc5849/test_client.py
@@ -2,9 +2,10 @@
from __future__ import absolute_import, unicode_literals
from oauthlib.common import Request
-from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_RSA,
+from oauthlib.oauth1 import (SIGNATURE_PLAINTEXT, SIGNATURE_HMAC_SHA1,
+ SIGNATURE_HMAC_SHA256, SIGNATURE_RSA,
SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY)
-from oauthlib.oauth1.rfc5849 import Client, bytes_type
+from oauthlib.oauth1.rfc5849 import Client
from ...unittest import TestCase
@@ -38,7 +39,7 @@ class ClientConstructorTests(TestCase):
def test_convert_to_unicode_resource_owner(self):
client = Client('client-key',
resource_owner_key=b'owner key')
- self.assertFalse(isinstance(client.resource_owner_key, bytes_type))
+ self.assertNotIsInstance(client.resource_owner_key, bytes)
self.assertEqual(client.resource_owner_key, 'owner key')
def test_give_explicit_timestamp(self):
@@ -56,19 +57,54 @@ class ClientConstructorTests(TestCase):
uri, headers, body = client.sign('http://a.b/path?query',
http_method='POST', body='a=b',
headers={'Content-Type': 'application/x-www-form-urlencoded'})
- self.assertIsInstance(uri, bytes_type)
- self.assertIsInstance(body, bytes_type)
+ self.assertIsInstance(uri, bytes)
+ self.assertIsInstance(body, bytes)
for k, v in headers.items():
- self.assertIsInstance(k, bytes_type)
- self.assertIsInstance(v, bytes_type)
+ self.assertIsInstance(k, bytes)
+ self.assertIsInstance(v, bytes)
+
+ def test_hmac_sha1(self):
+ client = Client('client_key')
+ # instance is using the correct signer method
+ self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA1],
+ client.SIGNATURE_METHODS[client.signature_method])
+
+ def test_hmac_sha256(self):
+ client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256)
+ # instance is using the correct signer method
+ self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_HMAC_SHA256],
+ client.SIGNATURE_METHODS[client.signature_method])
def test_rsa(self):
client = Client('client_key', signature_method=SIGNATURE_RSA)
- self.assertIsNone(client.rsa_key) # don't need an RSA key to instantiate
+ # instance is using the correct signer method
+ self.assertEqual(Client.SIGNATURE_METHODS[SIGNATURE_RSA],
+ client.SIGNATURE_METHODS[client.signature_method])
+ # don't need an RSA key to instantiate
+ self.assertIsNone(client.rsa_key)
class SignatureMethodTest(TestCase):
+ def test_hmac_sha1_method(self):
+ client = Client('client_key', timestamp='1234567890', nonce='abc')
+ u, h, b = client.sign('http://example.com')
+ correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", '
+ 'oauth_version="1.0", oauth_signature_method="HMAC-SHA1", '
+ 'oauth_consumer_key="client_key", '
+ 'oauth_signature="hH5BWYVqo7QI4EmPBUUe9owRUUQ%3D"')
+ self.assertEqual(h['Authorization'], correct)
+
+ def test_hmac_sha256_method(self):
+ client = Client('client_key', signature_method=SIGNATURE_HMAC_SHA256,
+ timestamp='1234567890', nonce='abc')
+ u, h, b = client.sign('http://example.com')
+ correct = ('OAuth oauth_nonce="abc", oauth_timestamp="1234567890", '
+ 'oauth_version="1.0", oauth_signature_method="HMAC-SHA256", '
+ 'oauth_consumer_key="client_key", '
+ 'oauth_signature="JzgJWBxX664OiMW3WE4MEjtYwOjI%2FpaUWHqtdHe68Es%3D"')
+ self.assertEqual(h['Authorization'], correct)
+
def test_rsa_method(self):
private_key = (
"-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDk1/bxy"
diff --git a/tests/oauth2/rfc6749/clients/test_backend_application.py b/tests/oauth2/rfc6749/clients/test_backend_application.py
index 6b342f0..aa2ba2b 100644
--- a/tests/oauth2/rfc6749/clients/test_backend_application.py
+++ b/tests/oauth2/rfc6749/clients/test_backend_application.py
@@ -15,6 +15,7 @@ from ....unittest import TestCase
class BackendApplicationClientTest(TestCase):
client_id = "someclientid"
+ client_secret = 'someclientsecret'
scope = ["/profile"]
kwargs = {
"some": "providers",
diff --git a/tests/oauth2/rfc6749/clients/test_base.py b/tests/oauth2/rfc6749/clients/test_base.py
index c788bc1..d48a944 100644
--- a/tests/oauth2/rfc6749/clients/test_base.py
+++ b/tests/oauth2/rfc6749/clients/test_base.py
@@ -4,7 +4,7 @@ from __future__ import absolute_import, unicode_literals
import datetime
from oauthlib import common
-from oauthlib.oauth2 import Client, InsecureTransportError
+from oauthlib.oauth2 import Client, InsecureTransportError, TokenExpiredError
from oauthlib.oauth2.rfc6749 import utils
from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY
@@ -51,10 +51,26 @@ class ClientTest(TestCase):
self.assertFormBodyEqual(body, self.body)
self.assertEqual(headers, self.bearer_header)
+ # Non-HTTPS
+ insecure_uri = 'http://example.com/path?query=world'
+ client = Client(self.client_id, access_token=self.access_token, token_type="Bearer")
+ self.assertRaises(InsecureTransportError, client.add_token, insecure_uri,
+ body=self.body,
+ headers=self.headers)
+
# Missing access token
client = Client(self.client_id)
self.assertRaises(ValueError, client.add_token, self.uri)
+ # Expired token
+ expired = 523549800
+ expired_token = {
+ 'expires_at': expired,
+ }
+ client = Client(self.client_id, token=expired_token, access_token=self.access_token, token_type="Bearer")
+ self.assertRaises(TokenExpiredError, client.add_token, self.uri,
+ body=self.body, headers=self.headers)
+
# The default token placement, bearer in auth header
client = Client(self.client_id, access_token=self.access_token)
uri, headers, body = client.add_token(self.uri, body=self.body,
@@ -150,8 +166,26 @@ class ClientTest(TestCase):
self.assertEqual(uri, self.uri)
self.assertEqual(body, self.body)
self.assertEqual(headers, self.mac_00_header)
+ # Non-HTTPS
+ insecure_uri = 'http://example.com/path?query=world'
+ self.assertRaises(InsecureTransportError, client.add_token, insecure_uri,
+ body=self.body,
+ headers=self.headers,
+ issue_time=datetime.datetime.now())
+ # Expired Token
+ expired = 523549800
+ expired_token = {
+ 'expires_at': expired,
+ }
+ client = Client(self.client_id, token=expired_token, token_type="MAC",
+ access_token=self.access_token, mac_key=self.mac_key,
+ mac_algorithm="hmac-sha-1")
+ self.assertRaises(TokenExpiredError, client.add_token, self.uri,
+ body=self.body,
+ headers=self.headers,
+ issue_time=datetime.datetime.now())
- # Add the Authorization header (draft 00)
+ # Add the Authorization header (draft 01)
client = Client(self.client_id, token_type="MAC",
access_token=self.access_token, mac_key=self.mac_key,
mac_algorithm="hmac-sha-1")
@@ -160,7 +194,24 @@ class ClientTest(TestCase):
self.assertEqual(uri, self.uri)
self.assertEqual(body, self.body)
self.assertEqual(headers, self.mac_01_header)
-
+ # Non-HTTPS
+ insecure_uri = 'http://example.com/path?query=world'
+ self.assertRaises(InsecureTransportError, client.add_token, insecure_uri,
+ body=self.body,
+ headers=self.headers,
+ draft=1)
+ # Expired Token
+ expired = 523549800
+ expired_token = {
+ 'expires_at': expired,
+ }
+ client = Client(self.client_id, token=expired_token, token_type="MAC",
+ access_token=self.access_token, mac_key=self.mac_key,
+ mac_algorithm="hmac-sha-1")
+ self.assertRaises(TokenExpiredError, client.add_token, self.uri,
+ body=self.body,
+ headers=self.headers,
+ draft=1)
def test_revocation_request(self):
client = Client(self.client_id)
@@ -208,6 +259,21 @@ class ClientTest(TestCase):
# NotImplementedError
self.assertRaises(NotImplementedError, client.prepare_authorization_request, auth_url)
+ def test_prepare_token_request(self):
+ redirect_url = 'https://example.com/callback/'
+ scopes = 'read'
+ token_url = 'https://example.com/token/'
+ state = 'fake_state'
+
+ client = Client(self.client_id, scope=scopes, state=state)
+
+ # Non-HTTPS
+ self.assertRaises(InsecureTransportError,
+ client.prepare_token_request, 'http://example.com/token/')
+
+ # NotImplementedError
+ self.assertRaises(NotImplementedError, client.prepare_token_request, token_url)
+
def test_prepare_refresh_token_request(self):
client = Client(self.client_id)
diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py
index 3f97c02..21af4a3 100644
--- a/tests/oauth2/rfc6749/clients/test_legacy_application.py
+++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py
@@ -10,19 +10,26 @@ from oauthlib.oauth2 import LegacyApplicationClient
from ....unittest import TestCase
+# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
@patch('time.time', new=lambda: 1000)
class LegacyApplicationClientTest(TestCase):
client_id = "someclientid"
+ client_secret = 'someclientsecret'
scope = ["/profile"]
kwargs = {
"some": "providers",
"require": "extra arguments"
}
- username = "foo"
- password = "bar"
+ username = "user_username"
+ password = "user_password"
body = "not=empty"
body_up = "not=empty&grant_type=password&username=%s&password=%s" % (username, password)
@@ -88,3 +95,54 @@ class LegacyApplicationClientTest(TestCase):
finally:
signals.scope_changed.disconnect(record_scope_change)
del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
+
+ def test_prepare_request_body(self):
+ """
+ see issue #585
+ https://github.com/oauthlib/oauthlib/issues/585
+ """
+ client = LegacyApplicationClient(self.client_id)
+
+ # scenario 1, default behavior to not include `client_id`
+ r1 = client.prepare_request_body(username=self.username, password=self.password)
+ self.assertIn(r1, ('grant_type=password&username=%s&password=%s' % (self.username, self.password, ),
+ 'grant_type=password&password=%s&username=%s' % (self.password, self.username, ),
+ ))
+
+ # scenario 2, include `client_id` in the body
+ r2 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True)
+ r2_params = dict(urlparse.parse_qsl(r2, keep_blank_values=True))
+ self.assertEqual(len(r2_params.keys()), 4)
+ self.assertEqual(r2_params['grant_type'], 'password')
+ self.assertEqual(r2_params['username'], self.username)
+ self.assertEqual(r2_params['password'], self.password)
+ self.assertEqual(r2_params['client_id'], self.client_id)
+
+ # scenario 3, include `client_id` + `client_secret` in the body
+ r3 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=self.client_secret)
+ r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True))
+ self.assertEqual(len(r3_params.keys()), 5)
+ self.assertEqual(r3_params['grant_type'], 'password')
+ self.assertEqual(r3_params['username'], self.username)
+ self.assertEqual(r3_params['password'], self.password)
+ self.assertEqual(r3_params['client_id'], self.client_id)
+ self.assertEqual(r3_params['client_secret'], self.client_secret)
+
+ # scenario 4, `client_secret` is an empty string
+ r4 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret='')
+ r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True))
+ self.assertEqual(len(r4_params.keys()), 5)
+ self.assertEqual(r4_params['grant_type'], 'password')
+ self.assertEqual(r4_params['username'], self.username)
+ self.assertEqual(r4_params['password'], self.password)
+ self.assertEqual(r4_params['client_id'], self.client_id)
+ self.assertEqual(r4_params['client_secret'], '')
+
+ # scenario 4b`,` client_secret is `None`
+ r4b = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=None)
+ r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True))
+ self.assertEqual(len(r4b_params.keys()), 4)
+ self.assertEqual(r4b_params['grant_type'], 'password')
+ self.assertEqual(r4b_params['username'], self.username)
+ self.assertEqual(r4b_params['password'], self.password)
+ self.assertEqual(r4b_params['client_id'], self.client_id)
diff --git a/tests/oauth2/rfc6749/clients/test_mobile_application.py b/tests/oauth2/rfc6749/clients/test_mobile_application.py
index 309220b..622b275 100644
--- a/tests/oauth2/rfc6749/clients/test_mobile_application.py
+++ b/tests/oauth2/rfc6749/clients/test_mobile_application.py
@@ -40,7 +40,7 @@ class MobileApplicationClientTest(TestCase):
token = {
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "example",
- "expires_in": "3600",
+ "expires_in": 3600,
"expires_at": 4600,
"scope": scope,
"example_parameter": "example_value"
@@ -69,6 +69,18 @@ class MobileApplicationClientTest(TestCase):
uri = client.prepare_request_uri(self.uri, **self.kwargs)
self.assertURLEqual(uri, self.uri_kwargs)
+ def test_populate_attributes(self):
+
+ client = MobileApplicationClient(self.client_id)
+
+ response_uri = (self.response_uri + "&code=EVIL-CODE")
+
+ client.parse_request_uri_response(response_uri, scope=self.scope)
+
+ # We must not accidentally pick up any further security
+ # credentials at this point.
+ self.assertIsNone(client.code)
+
def test_parse_token_response(self):
client = MobileApplicationClient(self.client_id)
diff --git a/tests/oauth2/rfc6749/clients/test_service_application.py b/tests/oauth2/rfc6749/clients/test_service_application.py
index 2dc633a..dc337cf 100644
--- a/tests/oauth2/rfc6749/clients/test_service_application.py
+++ b/tests/oauth2/rfc6749/clients/test_service_application.py
@@ -89,8 +89,8 @@ mfvGGg3xNjTMO7IdrwIDAQAB
audience=self.audience,
body=self.body)
r = Request('https://a.b', body=body)
- self.assertEqual(r.isnot, 'empty')
- self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type)
+ self.assertEqual(r.isnot, 'empty')
+ self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type)
claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256'])
@@ -98,6 +98,72 @@ mfvGGg3xNjTMO7IdrwIDAQAB
# audience verification is handled during decode now
self.assertEqual(claim['sub'], self.subject)
self.assertEqual(claim['iat'], int(t.return_value))
+ self.assertNotIn('nbf', claim)
+ self.assertNotIn('jti', claim)
+
+ # Missing issuer parameter
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=None, subject=self.subject, audience=self.audience, body=self.body)
+
+ # Missing subject parameter
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=self.issuer, subject=None, audience=self.audience, body=self.body)
+
+ # Missing audience parameter
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=self.issuer, subject=self.subject, audience=None, body=self.body)
+
+ # Optional kwargs
+ not_before = time() - 3600
+ jwt_id = '8zd15df4s35f43sd'
+ body = client.prepare_request_body(issuer=self.issuer,
+ subject=self.subject,
+ audience=self.audience,
+ body=self.body,
+ not_before=not_before,
+ jwt_id=jwt_id)
+
+ r = Request('https://a.b', body=body)
+ self.assertEqual(r.isnot, 'empty')
+ self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type)
+
+ claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256'])
+
+ self.assertEqual(claim['iss'], self.issuer)
+ # audience verification is handled during decode now
+ self.assertEqual(claim['sub'], self.subject)
+ self.assertEqual(claim['iat'], int(t.return_value))
+ self.assertEqual(claim['nbf'], not_before)
+ self.assertEqual(claim['jti'], jwt_id)
+
+ @patch('time.time')
+ def test_request_body_no_initial_private_key(self, t):
+ t.return_value = time()
+ self.token['expires_at'] = self.token['expires_in'] + t.return_value
+
+ client = ServiceApplicationClient(
+ self.client_id, private_key=None)
+
+ # Basic with private key provided
+ body = client.prepare_request_body(issuer=self.issuer,
+ subject=self.subject,
+ audience=self.audience,
+ body=self.body,
+ private_key=self.private_key)
+ r = Request('https://a.b', body=body)
+ self.assertEqual(r.isnot, 'empty')
+ self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type)
+
+ claim = jwt.decode(r.assertion, self.public_key, audience=self.audience, algorithms=['RS256'])
+
+ self.assertEqual(claim['iss'], self.issuer)
+ # audience verification is handled during decode now
+ self.assertEqual(claim['sub'], self.subject)
+ self.assertEqual(claim['iat'], int(t.return_value))
+
+ # No private key provided
+ self.assertRaises(ValueError, client.prepare_request_body,
+ issuer=self.issuer, subject=self.subject, audience=self.audience, body=self.body)
@patch('time.time')
def test_parse_token_response(self, t):
diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py
index 85b247d..092f93e 100644
--- a/tests/oauth2/rfc6749/clients/test_web_application.py
+++ b/tests/oauth2/rfc6749/clients/test_web_application.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
import datetime
import os
+import warnings
from mock import patch
@@ -15,11 +16,18 @@ from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY
from ....unittest import TestCase
+# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py
+try:
+ import urlparse
+except ImportError:
+ import urllib.parse as urlparse
+
@patch('time.time', new=lambda: 1000)
class WebApplicationClientTest(TestCase):
client_id = "someclientid"
+ client_secret = 'someclientsecret'
uri = "https://example.com/path?query=world"
uri_id = uri + "&response_type=code&client_id=" + client_id
uri_redirect = uri_id + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback"
@@ -117,6 +125,25 @@ class WebApplicationClientTest(TestCase):
self.response_uri,
state="invalid")
+ def test_populate_attributes(self):
+
+ client = WebApplicationClient(self.client_id)
+
+ response_uri = (self.response_uri +
+ "&access_token=EVIL-TOKEN"
+ "&refresh_token=EVIL-TOKEN"
+ "&mac_key=EVIL-KEY")
+
+ client.parse_request_uri_response(response_uri, self.state)
+
+ self.assertEqual(client.code, self.code)
+
+ # We must not accidentally pick up any further security
+ # credentials at this point.
+ self.assertIsNone(client.access_token)
+ self.assertIsNone(client.refresh_token)
+ self.assertIsNone(client.mac_key)
+
def test_parse_token_response(self):
client = WebApplicationClient(self.client_id)
@@ -158,3 +185,75 @@ class WebApplicationClientTest(TestCase):
# verify default header and body only
self.assertEqual(header, {'Content-Type': 'application/x-www-form-urlencoded'})
self.assertEqual(body, '')
+
+ def test_prepare_request_body(self):
+ """
+ see issue #585
+ https://github.com/oauthlib/oauthlib/issues/585
+
+ `prepare_request_body` should support the following scenarios:
+ 1. Include client_id alone in the body (default)
+ 2. Include client_id and client_secret in auth and not include them in the body (RFC preferred solution)
+ 3. Include client_id and client_secret in the body (RFC alternative solution)
+ 4. Include client_id in the body and an empty string for client_secret.
+ """
+ client = WebApplicationClient(self.client_id)
+
+ # scenario 1, default behavior to include `client_id`
+ r1 = client.prepare_request_body()
+ self.assertEqual(r1, 'grant_type=authorization_code&client_id=%s' % self.client_id)
+
+ r1b = client.prepare_request_body(include_client_id=True)
+ self.assertEqual(r1b, 'grant_type=authorization_code&client_id=%s' % self.client_id)
+
+ # scenario 2, do not include `client_id` in the body, so it can be sent in auth.
+ r2 = client.prepare_request_body(include_client_id=False)
+ self.assertEqual(r2, 'grant_type=authorization_code')
+
+ # scenario 3, Include client_id and client_secret in the body (RFC alternative solution)
+ # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting
+ r3 = client.prepare_request_body(client_secret=self.client_secret)
+ r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True))
+ self.assertEqual(len(r3_params.keys()), 3)
+ self.assertEqual(r3_params['grant_type'], 'authorization_code')
+ self.assertEqual(r3_params['client_id'], self.client_id)
+ self.assertEqual(r3_params['client_secret'], self.client_secret)
+
+ r3b = client.prepare_request_body(include_client_id=True, client_secret=self.client_secret)
+ r3b_params = dict(urlparse.parse_qsl(r3b, keep_blank_values=True))
+ self.assertEqual(len(r3b_params.keys()), 3)
+ self.assertEqual(r3b_params['grant_type'], 'authorization_code')
+ self.assertEqual(r3b_params['client_id'], self.client_id)
+ self.assertEqual(r3b_params['client_secret'], self.client_secret)
+
+ # scenario 4, `client_secret` is an empty string
+ r4 = client.prepare_request_body(include_client_id=True, client_secret='')
+ r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True))
+ self.assertEqual(len(r4_params.keys()), 3)
+ self.assertEqual(r4_params['grant_type'], 'authorization_code')
+ self.assertEqual(r4_params['client_id'], self.client_id)
+ self.assertEqual(r4_params['client_secret'], '')
+
+ # scenario 4b, `client_secret` is `None`
+ r4b = client.prepare_request_body(include_client_id=True, client_secret=None)
+ r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True))
+ self.assertEqual(len(r4b_params.keys()), 2)
+ self.assertEqual(r4b_params['grant_type'], 'authorization_code')
+ self.assertEqual(r4b_params['client_id'], self.client_id)
+
+ # scenario Warnings
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always") # catch all
+
+ # warning1 - raise a DeprecationWarning if a `client_id` is submitted
+ rWarnings1 = client.prepare_request_body(client_id=self.client_id)
+ self.assertEqual(len(w), 1)
+ self.assertIsInstance(w[0].message, DeprecationWarning)
+
+ # testing the exact warning message in Python2&Python3 is a pain
+
+ # scenario Exceptions
+ # exception1 - raise a ValueError if the a different `client_id` is submitted
+ with self.assertRaises(ValueError) as cm:
+ client.prepare_request_body(client_id='different_client_id')
+ # testing the exact exception message in Python2&Python3 is a pain
diff --git a/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py
index 4ad0ed9..4f78d9b 100644
--- a/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py
+++ b/tests/oauth2/rfc6749/endpoints/test_base_endpoint.py
@@ -24,7 +24,9 @@ class BaseEndpointTest(TestCase):
validator = RequestValidator()
server = Server(validator)
server.catch_errors = True
- h, b, s = server.create_authorization_response('https://example.com')
+ h, b, s = server.create_token_response(
+ 'https://example.com?grant_type=authorization_code&code=abc'
+ )
self.assertIn("server_error", b)
self.assertEqual(s, 500)
diff --git a/tests/oauth2/rfc6749/endpoints/test_client_authentication.py b/tests/oauth2/rfc6749/endpoints/test_client_authentication.py
index e9a0673..133da59 100644
--- a/tests/oauth2/rfc6749/endpoints/test_client_authentication.py
+++ b/tests/oauth2/rfc6749/endpoints/test_client_authentication.py
@@ -32,6 +32,8 @@ class ClientAuthenticationTest(TestCase):
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.is_pkce_required.return_value = False
+ self.validator.get_code_challenge.return_value = None
self.validator.get_default_redirect_uri.return_value = 'http://i.b./path'
self.web = WebApplicationServer(self.validator,
token_generator=self.inspect_client)
@@ -41,6 +43,11 @@ class ClientAuthenticationTest(TestCase):
token_generator=self.inspect_client)
self.backend = BackendApplicationServer(self.validator,
token_generator=self.inspect_client)
+ self.token_uri = 'http://example.com/path'
+ self.auth_uri = 'http://example.com/path?client_id=abc&response_type=token'
+ # should be base64 but no added value in this unittest
+ self.basicauth_client_creds = {"Authorization": "john:doe"}
+ self.basicauth_client_id = {"Authorization": "john:"}
def set_client(self, request):
request.client = mock.MagicMock()
@@ -52,7 +59,9 @@ class ClientAuthenticationTest(TestCase):
request.client.client_id = 'mocked'
return True
- def set_username(self, username, password, client, request):
+ def basicauth_authenticate_client(self, request):
+ assert "Authorization" in request.headers
+ assert "john:doe" in request.headers["Authorization"]
request.client = mock.MagicMock()
request.client.client_id = 'mocked'
return True
@@ -84,6 +93,55 @@ class ClientAuthenticationTest(TestCase):
self.assertIn('Location', h)
self.assertIn('access_token', get_fragment_credentials(h['Location']))
+ def test_basicauth_web(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+ _, body, _ = self.web.create_token_response(
+ self.token_uri,
+ body='grant_type=authorization_code&code=mock',
+ headers=self.basicauth_client_creds
+ )
+ self.assertIn('access_token', json.loads(body))
+
+ def test_basicauth_legacy(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+ _, body, _ = self.legacy.create_token_response(
+ self.token_uri,
+ body='grant_type=password&username=abc&password=secret',
+ headers=self.basicauth_client_creds
+ )
+ self.assertIn('access_token', json.loads(body))
+
+ def test_basicauth_backend(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+ _, body, _ = self.backend.create_token_response(
+ self.token_uri,
+ body='grant_type=client_credentials',
+ headers=self.basicauth_client_creds
+ )
+ self.assertIn('access_token', json.loads(body))
+
+ def test_basicauth_revoke(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+
+ # legacy or any other uses the same RevocationEndpoint
+ _, body, status = self.legacy.create_revocation_response(
+ self.token_uri,
+ body='token=foobar',
+ headers=self.basicauth_client_creds
+ )
+ self.assertEqual(status, 200, body)
+
+ def test_basicauth_introspect(self):
+ self.validator.authenticate_client.side_effect = self.basicauth_authenticate_client
+
+ # legacy or any other uses the same IntrospectEndpoint
+ _, body, status = self.legacy.create_introspect_response(
+ self.token_uri,
+ body='token=foobar',
+ headers=self.basicauth_client_creds
+ )
+ self.assertEqual(status, 200, body)
+
def test_custom_authentication(self):
token_uri = 'http://example.com/path'
diff --git a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py
index 0eb719f..1a2f66b 100644
--- a/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py
+++ b/tests/oauth2/rfc6749/endpoints/test_credentials_preservation.py
@@ -24,6 +24,7 @@ class PreservationTest(TestCase):
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
self.validator.get_default_redirect_uri.return_value = self.DEFAULT_REDIRECT_URI
+ self.validator.get_code_challenge.return_value = None
self.validator.authenticate_client.side_effect = self.set_client
self.web = WebApplicationServer(self.validator)
self.mobile = MobileApplicationServer(self.validator)
@@ -116,3 +117,24 @@ class PreservationTest(TestCase):
self.assertRaises(errors.MissingRedirectURIError,
self.mobile.create_authorization_response,
auth_uri + '&response_type=token', scopes=['random'])
+
+ def test_default_uri_in_token(self):
+ auth_uri = 'http://example.com/path?state=xyz&client_id=abc'
+ token_uri = 'http://example.com/path'
+
+ # authorization grant
+ h, _, s = self.web.create_authorization_response(
+ auth_uri + '&response_type=code', scopes=['random'])
+ self.assertEqual(s, 302)
+ self.assertIn('Location', h)
+ self.assertTrue(h['Location'].startswith(self.DEFAULT_REDIRECT_URI))
+
+ # confirm_redirect_uri should return true if the redirect uri
+ # was not given in the authorization AND not in the token request.
+ self.validator.confirm_redirect_uri.return_value = True
+ code = get_query_credentials(h['Location'])['code'][0]
+ self.validator.validate_code.side_effect = self.set_state('xyz')
+ _, body, s = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=%s' % code)
+ self.assertEqual(s, 200)
+ self.assertEqual(self.validator.confirm_redirect_uri.call_args[0][2], self.DEFAULT_REDIRECT_URI)
diff --git a/tests/oauth2/rfc6749/endpoints/test_error_responses.py b/tests/oauth2/rfc6749/endpoints/test_error_responses.py
index 875b3a5..a249cb1 100644
--- a/tests/oauth2/rfc6749/endpoints/test_error_responses.py
+++ b/tests/oauth2/rfc6749/endpoints/test_error_responses.py
@@ -24,6 +24,7 @@ class ErrorResponseTest(TestCase):
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
self.validator.get_default_redirect_uri.return_value = None
+ self.validator.get_code_challenge.return_value = None
self.web = WebApplicationServer(self.validator)
self.mobile = MobileApplicationServer(self.validator)
self.legacy = LegacyApplicationServer(self.validator)
@@ -44,6 +45,22 @@ class ErrorResponseTest(TestCase):
self.assertRaises(errors.InvalidRedirectURIError,
self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+ def test_invalid_default_redirect_uri(self):
+ uri = 'https://example.com/authorize?response_type={0}&client_id=foo'
+ self.validator.get_default_redirect_uri.return_value = "wrong"
+
+ # Authorization code grant
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.web.validate_authorization_request, uri.format('code'))
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.web.create_authorization_response, uri.format('code'), scopes=['foo'])
+
+ # Implicit grant
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.mobile.validate_authorization_request, uri.format('token'))
+ self.assertRaises(errors.InvalidRedirectURIError,
+ self.mobile.create_authorization_response, uri.format('token'), scopes=['foo'])
+
def test_missing_redirect_uri(self):
uri = 'https://example.com/authorize?response_type={0}&client_id=foo'
@@ -237,6 +254,7 @@ class ErrorResponseTest(TestCase):
def test_access_denied(self):
self.validator.authenticate_client.side_effect = self.set_client
+ self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
self.validator.confirm_redirect_uri.return_value = False
token_uri = 'https://i.b/token'
# Authorization code grant
@@ -244,6 +262,15 @@ class ErrorResponseTest(TestCase):
body='grant_type=authorization_code&code=foo')
self.assertEqual('invalid_request', json.loads(body)['error'])
+ def test_access_denied_no_default_redirecturi(self):
+ self.validator.authenticate_client.side_effect = self.set_client
+ self.validator.get_default_redirect_uri.return_value = None
+ token_uri = 'https://i.b/token'
+ # Authorization code grant
+ _, body, _ = self.web.create_token_response(token_uri,
+ body='grant_type=authorization_code&code=foo')
+ self.assertEqual('invalid_request', json.loads(body)['error'])
+
def test_unsupported_response_type(self):
self.validator.get_default_redirect_uri.return_value = 'https://i.b/cb'
diff --git a/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py
new file mode 100644
index 0000000..b9bf76a
--- /dev/null
+++ b/tests/oauth2/rfc6749/endpoints/test_introspect_endpoint.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+from json import loads
+
+from mock import MagicMock
+
+from oauthlib.common import urlencode
+from oauthlib.oauth2 import RequestValidator, IntrospectEndpoint
+
+from ....unittest import TestCase
+
+
+class IntrospectEndpointTest(TestCase):
+
+ def setUp(self):
+ self.validator = MagicMock(wraps=RequestValidator())
+ self.validator.client_authentication_required.return_value = True
+ self.validator.authenticate_client.return_value = True
+ self.validator.validate_bearer_token.return_value = True
+ self.validator.introspect_token.return_value = {}
+ self.endpoint = IntrospectEndpoint(self.validator)
+
+ self.uri = 'should_not_matter'
+ self.headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ }
+ self.resp_h = {
+ 'Cache-Control': 'no-store',
+ 'Content-Type': 'application/json',
+ 'Pragma': 'no-cache'
+ }
+ self.resp_b = {
+ "active": True
+ }
+
+ def test_introspect_token(self):
+ for token_type in ('access_token', 'refresh_token', 'invalid'):
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', token_type)])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), self.resp_b)
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_nohint(self):
+ # don't specify token_type_hint
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), self.resp_b)
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_false(self):
+ self.validator.introspect_token.return_value = None
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), {"active": False})
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_claims(self):
+ self.validator.introspect_token.return_value = {"foo": "bar"}
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), {"active": True, "foo": "bar"})
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_claims_spoof_active(self):
+ self.validator.introspect_token.return_value = {"foo": "bar", "active": False}
+ body = urlencode([('token', 'foo')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), {"active": True, "foo": "bar"})
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_client_authentication_failed(self):
+ self.validator.authenticate_client.return_value = False
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer, error="invalid_client"'
+ })
+ self.assertEqual(loads(b)['error'], 'invalid_client')
+ self.assertEqual(s, 401)
+
+ def test_introspect_token_public_client_authentication(self):
+ self.validator.client_authentication_required.return_value = False
+ self.validator.authenticate_client_id.return_value = True
+ for token_type in ('access_token', 'refresh_token', 'invalid'):
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', token_type)])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b), self.resp_b)
+ self.assertEqual(s, 200)
+
+ def test_introspect_token_public_client_authentication_failed(self):
+ self.validator.client_authentication_required.return_value = False
+ self.validator.authenticate_client_id.return_value = False
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'access_token')])
+ h, b, s = self.endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer, error="invalid_client"'
+ })
+ self.assertEqual(loads(b)['error'], 'invalid_client')
+ self.assertEqual(s, 401)
+
+ def test_introspect_unsupported_token(self):
+ endpoint = IntrospectEndpoint(self.validator,
+ supported_token_types=['access_token'])
+ body = urlencode([('token', 'foo'),
+ ('token_type_hint', 'refresh_token')])
+ h, b, s = endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body=body)
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'unsupported_token_type')
+ self.assertEqual(s, 400)
+
+ h, b, s = endpoint.create_introspect_response(self.uri,
+ headers=self.headers, body='')
+ self.assertEqual(h, self.resp_h)
+ self.assertEqual(loads(b)['error'], 'invalid_request')
+ self.assertEqual(s, 400)
diff --git a/tests/oauth2/rfc6749/endpoints/test_metadata.py b/tests/oauth2/rfc6749/endpoints/test_metadata.py
new file mode 100644
index 0000000..4813b46
--- /dev/null
+++ b/tests/oauth2/rfc6749/endpoints/test_metadata.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+from oauthlib.oauth2 import MetadataEndpoint
+from oauthlib.oauth2 import TokenEndpoint
+from oauthlib.oauth2 import Server
+
+from ....unittest import TestCase
+
+
+class MetadataEndpointTest(TestCase):
+ def setUp(self):
+ self.metadata = {
+ "issuer": 'https://foo.bar'
+ }
+
+ def test_openid_oauth2_preconfigured(self):
+ default_claims = {
+ "issuer": 'https://foo.bar',
+ "authorization_endpoint": "https://foo.bar/authorize",
+ "revocation_endpoint": "https://foo.bar/revoke",
+ "introspection_endpoint": "https://foo.bar/introspect",
+ "token_endpoint": "https://foo.bar/token"
+ }
+ from oauthlib.oauth2 import Server as OAuth2Server
+ from oauthlib.openid import Server as OpenIDServer
+
+ endpoint = OAuth2Server(None)
+ metadata = MetadataEndpoint([endpoint], default_claims)
+ oauth2_claims = metadata.claims
+
+ endpoint = OpenIDServer(None)
+ metadata = MetadataEndpoint([endpoint], default_claims)
+ openid_claims = metadata.claims
+
+ # Pure OAuth2 Authorization Metadata are similar with OpenID but
+ # response_type not! (OIDC contains "id_token" and hybrid flows)
+ del oauth2_claims['response_types_supported']
+ del openid_claims['response_types_supported']
+
+ self.maxDiff = None
+ self.assertEqual(openid_claims, oauth2_claims)
+
+ def test_token_endpoint(self):
+ endpoint = TokenEndpoint(None, None, grant_types={"password": None})
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "token_endpoint": "https://foo.bar/token"
+ })
+ self.assertIn("grant_types_supported", metadata.claims)
+ self.assertEqual(metadata.claims["grant_types_supported"], ["password"])
+
+ def test_token_endpoint_overridden(self):
+ endpoint = TokenEndpoint(None, None, grant_types={"password": None})
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "token_endpoint": "https://foo.bar/token",
+ "grant_types_supported": ["pass_word_special_provider"]
+ })
+ self.assertIn("grant_types_supported", metadata.claims)
+ self.assertEqual(metadata.claims["grant_types_supported"], ["pass_word_special_provider"])
+
+ def test_mandatory_fields(self):
+ metadata = MetadataEndpoint([], self.metadata)
+ self.assertIn("issuer", metadata.claims)
+ self.assertEqual(metadata.claims["issuer"], 'https://foo.bar')
+
+ def test_server_metadata(self):
+ endpoint = Server(None)
+ metadata = MetadataEndpoint([endpoint], {
+ "issuer": 'https://foo.bar',
+ "authorization_endpoint": "https://foo.bar/authorize",
+ "introspection_endpoint": "https://foo.bar/introspect",
+ "revocation_endpoint": "https://foo.bar/revoke",
+ "token_endpoint": "https://foo.bar/token",
+ "jwks_uri": "https://foo.bar/certs",
+ "scopes_supported": ["email", "profile"]
+ })
+ expected_claims = {
+ "issuer": "https://foo.bar",
+ "authorization_endpoint": "https://foo.bar/authorize",
+ "introspection_endpoint": "https://foo.bar/introspect",
+ "revocation_endpoint": "https://foo.bar/revoke",
+ "token_endpoint": "https://foo.bar/token",
+ "jwks_uri": "https://foo.bar/certs",
+ "scopes_supported": ["email", "profile"],
+ "grant_types_supported": [
+ "authorization_code",
+ "password",
+ "client_credentials",
+ "refresh_token",
+ "implicit"
+ ],
+ "token_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ],
+ "response_types_supported": [
+ "code",
+ "token"
+ ],
+ "response_modes_supported": [
+ "query",
+ "fragment"
+ ],
+ "code_challenge_methods_supported": [
+ "plain",
+ "S256"
+ ],
+ "revocation_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ],
+ "introspection_endpoint_auth_methods_supported": [
+ "client_secret_post",
+ "client_secret_basic"
+ ]
+ }
+
+ def sort_list(claims):
+ for k in claims.keys():
+ claims[k] = sorted(claims[k])
+
+ sort_list(metadata.claims)
+ sort_list(expected_claims)
+ self.assertEqual(sorted(metadata.claims.items()), sorted(expected_claims.items()))
diff --git a/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py b/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py
index d30ec9d..e823286 100644
--- a/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py
+++ b/tests/oauth2/rfc6749/endpoints/test_resource_owner_association.py
@@ -46,6 +46,7 @@ class ResourceOwnerAssociationTest(TestCase):
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
self.validator.get_default_redirect_uri.return_value = 'http://i.b./path'
+ self.validator.get_code_challenge.return_value = None
self.validator.authenticate_client.side_effect = self.set_client
self.web = WebApplicationServer(self.validator,
token_generator=self.inspect_client)
diff --git a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py
index 77f5662..2a24177 100644
--- a/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py
+++ b/tests/oauth2/rfc6749/endpoints/test_revocation_endpoint.py
@@ -24,6 +24,11 @@ class RevocationEndpointTest(TestCase):
self.headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
+ self.resp_h = {
+ 'Cache-Control': 'no-store',
+ 'Content-Type': 'application/json',
+ 'Pragma': 'no-cache'
+ }
def test_revoke_token(self):
for token_type in ('access_token', 'refresh_token', 'invalid'):
@@ -49,7 +54,12 @@ class RevocationEndpointTest(TestCase):
('token_type_hint', 'access_token')])
h, b, s = self.endpoint.create_revocation_response(self.uri,
headers=self.headers, body=body)
- self.assertEqual(h, {})
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer, error="invalid_client"'
+ })
self.assertEqual(loads(b)['error'], 'invalid_client')
self.assertEqual(s, 401)
@@ -72,7 +82,12 @@ class RevocationEndpointTest(TestCase):
('token_type_hint', 'access_token')])
h, b, s = self.endpoint.create_revocation_response(self.uri,
headers=self.headers, body=body)
- self.assertEqual(h, {})
+ self.assertEqual(h, {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'Pragma': 'no-cache',
+ "WWW-Authenticate": 'Bearer, error="invalid_client"'
+ })
self.assertEqual(loads(b)['error'], 'invalid_client')
self.assertEqual(s, 401)
@@ -96,12 +111,12 @@ class RevocationEndpointTest(TestCase):
('token_type_hint', 'refresh_token')])
h, b, s = endpoint.create_revocation_response(self.uri,
headers=self.headers, body=body)
- self.assertEqual(h, {})
+ self.assertEqual(h, self.resp_h)
self.assertEqual(loads(b)['error'], 'unsupported_token_type')
self.assertEqual(s, 400)
h, b, s = endpoint.create_revocation_response(self.uri,
headers=self.headers, body='')
- self.assertEqual(h, {})
+ self.assertEqual(h, self.resp_h)
self.assertEqual(loads(b)['error'], 'invalid_request')
self.assertEqual(s, 400)
diff --git a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
index 8490c03..4f27963 100644
--- a/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
+++ b/tests/oauth2/rfc6749/endpoints/test_scope_handling.py
@@ -42,6 +42,7 @@ class TestScopeHandling(TestCase):
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
self.validator.get_default_redirect_uri.return_value = TestScopeHandling.DEFAULT_REDIRECT_URI
+ self.validator.get_code_challenge.return_value = None
self.validator.authenticate_client.side_effect = self.set_client
self.server = Server(self.validator)
self.web = WebApplicationServer(self.validator)
diff --git a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py
index 704a254..00e2b6d 100644
--- a/tests/oauth2/rfc6749/grant_types/test_authorization_code.py
+++ b/tests/oauth2/rfc6749/grant_types/test_authorization_code.py
@@ -8,6 +8,7 @@ import mock
from oauthlib.common import Request
from oauthlib.oauth2.rfc6749 import errors
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant
+from oauthlib.oauth2.rfc6749.grant_types import authorization_code
from oauthlib.oauth2.rfc6749.tokens import BearerToken
from ....unittest import TestCase
@@ -27,6 +28,8 @@ class AuthorizationCodeGrantTest(TestCase):
self.request.redirect_uri = 'https://a.b/cb'
self.mock_validator = mock.MagicMock()
+ self.mock_validator.is_pkce_required.return_value = False
+ self.mock_validator.get_code_challenge.return_value = None
self.mock_validator.authenticate_client.side_effect = self.set_client
self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator)
@@ -77,6 +80,12 @@ class AuthorizationCodeGrantTest(TestCase):
self.assertTrue(self.mock_validator.validate_response_type.called)
self.assertTrue(self.mock_validator.validate_scopes.called)
+ def test_create_authorization_grant_no_scopes(self):
+ bearer = BearerToken(self.mock_validator)
+ self.request.response_mode = 'query'
+ self.request.scopes = []
+ self.auth.create_authorization_response(self.request, bearer)
+
def test_create_authorization_grant_state(self):
self.request.state = 'abc'
self.request.redirect_uri = None
@@ -194,3 +203,124 @@ class AuthorizationCodeGrantTest(TestCase):
self.mock_validator.confirm_redirect_uri.return_value = False
self.assertRaises(errors.MismatchingRedirectURIError,
self.auth.validate_token_request, self.request)
+
+ # PKCE validate_authorization_request
+ def test_pkce_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.assertRaises(errors.MissingCodeChallengeError,
+ self.auth.validate_authorization_request, self.request)
+
+ def test_pkce_default_method(self):
+ for required in [True, False]:
+ self.mock_validator.is_pkce_required.return_value = required
+ self.request.code_challenge = "present"
+ _, ri = self.auth.validate_authorization_request(self.request)
+ self.assertIsNotNone(ri["request"].code_challenge_method)
+ self.assertEqual(ri["request"].code_challenge_method, "plain")
+
+ def test_pkce_wrong_method(self):
+ for required in [True, False]:
+ self.mock_validator.is_pkce_required.return_value = required
+ self.request.code_challenge = "present"
+ self.request.code_challenge_method = "foobar"
+ self.assertRaises(errors.UnsupportedCodeChallengeMethodError,
+ self.auth.validate_authorization_request, self.request)
+
+ # PKCE validate_token_request
+ def test_pkce_verifier_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ # PKCE validate_token_request
+ def test_pkce_required_verifier_missing_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = None
+ self.mock_validator.get_code_challenge.return_value = None
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_missing_challenge_valid(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = None
+ self.mock_validator.get_code_challenge.return_value = "foo"
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_valid_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "foobar"
+ self.mock_validator.get_code_challenge.return_value = None
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_valid_challenge_valid_method_valid(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "foobar"
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.mock_validator.get_code_challenge_method.return_value = "plain"
+ self.auth.validate_token_request(self.request)
+
+ def test_pkce_required_verifier_invalid_challenge_valid_method_valid(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "foobar"
+ self.mock_validator.get_code_challenge.return_value = "raboof"
+ self.mock_validator.get_code_challenge_method.return_value = "plain"
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_required_verifier_valid_challenge_valid_method_wrong(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "present"
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.mock_validator.get_code_challenge_method.return_value = "cryptic_method"
+ self.assertRaises(errors.ServerError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_verifier_valid_challenge_valid_method_missing(self):
+ self.mock_validator.is_pkce_required.return_value = True
+ self.request.code_verifier = "present"
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.mock_validator.get_code_challenge_method.return_value = None
+ self.assertRaises(errors.InvalidGrantError,
+ self.auth.validate_token_request, self.request)
+
+ def test_pkce_optional_verifier_valid_challenge_missing(self):
+ self.mock_validator.is_pkce_required.return_value = False
+ self.request.code_verifier = "present"
+ self.mock_validator.get_code_challenge.return_value = None
+ self.auth.validate_token_request(self.request)
+
+ def test_pkce_optional_verifier_missing_challenge_valid(self):
+ self.mock_validator.is_pkce_required.return_value = False
+ self.request.code_verifier = None
+ self.mock_validator.get_code_challenge.return_value = "foobar"
+ self.assertRaises(errors.MissingCodeVerifierError,
+ self.auth.validate_token_request, self.request)
+
+ # PKCE functions
+ def test_wrong_code_challenge_method_plain(self):
+ self.assertFalse(authorization_code.code_challenge_method_plain("foo", "bar"))
+
+ def test_correct_code_challenge_method_plain(self):
+ self.assertTrue(authorization_code.code_challenge_method_plain("foo", "foo"))
+
+ def test_wrong_code_challenge_method_s256(self):
+ self.assertFalse(authorization_code.code_challenge_method_s256("foo", "bar"))
+
+ def test_correct_code_challenge_method_s256(self):
+ # "abcd" as verifier gives a '+' to base64
+ self.assertTrue(
+ authorization_code.code_challenge_method_s256("abcd",
+ "iNQmb9TmM40TuEX88olXnSCciXgjuSF9o-Fhk28DFYk")
+ )
+ # "/" as verifier gives a '/' and '+' to base64
+ self.assertTrue(
+ authorization_code.code_challenge_method_s256("/",
+ "il7asoJjJEMhngUeSt4tHVu8Zxx4EFG_FDeJfL3-oPE")
+ )
+ # Example from PKCE RFCE
+ self.assertTrue(
+ authorization_code.code_challenge_method_s256("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
+ "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
+ )
diff --git a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py b/tests/oauth2/rfc6749/grant_types/test_openid_connect.py
deleted file mode 100644
index 573d491..0000000
--- a/tests/oauth2/rfc6749/grant_types/test_openid_connect.py
+++ /dev/null
@@ -1,403 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
-
-import json
-
-import mock
-
-from oauthlib.common import Request
-from oauthlib.oauth2.rfc6749.grant_types import (AuthTokenGrantDispatcher,
- AuthorizationCodeGrant,
- ImplicitGrant,
- ImplicitTokenGrantDispatcher,
- OIDCNoPrompt,
- OpenIDConnectAuthCode,
- OpenIDConnectHybrid,
- OpenIDConnectImplicit)
-from oauthlib.oauth2.rfc6749.tokens import BearerToken
-
-from ....unittest import TestCase
-from .test_authorization_code import AuthorizationCodeGrantTest
-from .test_implicit import ImplicitGrantTest
-
-
-class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest):
- """Test that OpenID don't interfere with normal OAuth 2 flows."""
-
- def setUp(self):
- super(OpenIDAuthCodeInterferenceTest, self).setUp()
- self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator)
-
-
-class OpenIDImplicitInterferenceTest(ImplicitGrantTest):
- """Test that OpenID don't interfere with normal OAuth 2 flows."""
-
- def setUp(self):
- super(OpenIDImplicitInterferenceTest, self).setUp()
- self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator)
-
-
-class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest):
- """Test that OpenID don't interfere with normal OAuth 2 flows."""
-
- def setUp(self):
- super(OpenIDHybridInterferenceTest, self).setUp()
- self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator)
-
-
-def get_id_token_mock(token, token_handler, request):
- return "MOCKED_TOKEN"
-
-
-class OpenIDAuthCodeTest(TestCase):
-
- def setUp(self):
- self.request = Request('http://a.b/path')
- self.request.scopes = ('hello', 'openid')
- self.request.expires_in = 1800
- self.request.client_id = 'abcdef'
- self.request.code = '1234'
- self.request.response_type = 'code'
- self.request.grant_type = 'authorization_code'
- self.request.redirect_uri = 'https://a.b/cb'
- self.request.state = 'abc'
-
- self.mock_validator = mock.MagicMock()
- self.mock_validator.authenticate_client.side_effect = self.set_client
- self.mock_validator.get_id_token.side_effect = get_id_token_mock
- self.auth = OpenIDConnectAuthCode(request_validator=self.mock_validator)
-
- self.url_query = 'https://a.b/cb?code=abc&state=abc'
- self.url_fragment = 'https://a.b/cb#code=abc&state=abc'
-
- def set_client(self, request):
- request.client = mock.MagicMock()
- request.client.client_id = 'mocked'
- return True
-
- @mock.patch('oauthlib.common.generate_token')
- def test_authorization(self, generate_token):
-
- scope, info = self.auth.validate_authorization_request(self.request)
-
- generate_token.return_value = 'abc'
- bearer = BearerToken(self.mock_validator)
- self.request.response_mode = 'query'
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], self.url_query)
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- self.request.response_mode = 'fragment'
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- @mock.patch('oauthlib.common.generate_token')
- def test_no_prompt_authorization(self, generate_token):
- generate_token.return_value = 'abc'
- scope, info = self.auth.validate_authorization_request(self.request)
- self.request.prompt = 'none'
- self.assertRaises(OIDCNoPrompt,
- self.auth.validate_authorization_request,
- self.request)
-
- # prompt == none requires id token hint
- bearer = BearerToken(self.mock_validator)
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=invalid_request', h['Location'])
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- self.request.response_mode = 'query'
- self.request.id_token_hint = 'me@email.com'
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], self.url_query)
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- # Test alernative response modes
- self.request.response_mode = 'fragment'
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
-
- # Ensure silent authentication and authorization is done
- self.mock_validator.validate_silent_login.return_value = False
- self.mock_validator.validate_silent_authorization.return_value = True
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=login_required', h['Location'])
-
- self.mock_validator.validate_silent_login.return_value = True
- self.mock_validator.validate_silent_authorization.return_value = False
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=consent_required', h['Location'])
-
- # ID token hint must match logged in user
- self.mock_validator.validate_silent_authorization.return_value = True
- self.mock_validator.validate_user_match.return_value = False
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=login_required', h['Location'])
-
- def set_scopes(self, client_id, code, client, request):
- request.scopes = self.request.scopes
- request.state = self.request.state
- request.user = 'bob'
- return True
-
- def test_create_token_response(self):
- self.request.response_type = None
- self.mock_validator.validate_code.side_effect = self.set_scopes
-
- bearer = BearerToken(self.mock_validator)
-
- h, token, s = self.auth.create_token_response(self.request, bearer)
- token = json.loads(token)
- self.assertEqual(self.mock_validator.save_token.call_count, 1)
- self.assertIn('access_token', token)
- self.assertIn('refresh_token', token)
- self.assertIn('expires_in', token)
- self.assertIn('scope', token)
- self.assertIn('id_token', token)
- self.assertIn('openid', token['scope'])
-
- self.mock_validator.reset_mock()
-
- self.request.scopes = ('hello', 'world')
- h, token, s = self.auth.create_token_response(self.request, bearer)
- token = json.loads(token)
- self.assertEqual(self.mock_validator.save_token.call_count, 1)
- self.assertIn('access_token', token)
- self.assertIn('refresh_token', token)
- self.assertIn('expires_in', token)
- self.assertIn('scope', token)
- self.assertNotIn('id_token', token)
- self.assertNotIn('openid', token['scope'])
-
-
-class OpenIDImplicitTest(TestCase):
-
- def setUp(self):
- self.request = Request('http://a.b/path')
- self.request.scopes = ('hello', 'openid')
- self.request.expires_in = 1800
- self.request.client_id = 'abcdef'
- self.request.response_type = 'id_token token'
- self.request.redirect_uri = 'https://a.b/cb'
- self.request.nonce = 'zxc'
- self.request.state = 'abc'
-
- self.mock_validator = mock.MagicMock()
- self.mock_validator.get_id_token.side_effect = get_id_token_mock
- self.auth = OpenIDConnectImplicit(request_validator=self.mock_validator)
-
- token = 'MOCKED_TOKEN'
- self.url_query = 'https://a.b/cb?state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
- self.url_fragment = 'https://a.b/cb#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
-
- @mock.patch('oauthlib.common.generate_token')
- def test_authorization(self, generate_token):
- scope, info = self.auth.validate_authorization_request(self.request)
-
- generate_token.return_value = 'abc'
- bearer = BearerToken(self.mock_validator)
-
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- self.request.response_type = 'id_token'
- token = 'MOCKED_TOKEN'
- url = 'https://a.b/cb#state=abc&id_token=%s' % token
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], url, parse_fragment=True)
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- self.request.nonce = None
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=invalid_request', h['Location'])
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- @mock.patch('oauthlib.common.generate_token')
- def test_no_prompt_authorization(self, generate_token):
- generate_token.return_value = 'abc'
- scope, info = self.auth.validate_authorization_request(self.request)
- self.request.prompt = 'none'
- self.assertRaises(OIDCNoPrompt,
- self.auth.validate_authorization_request,
- self.request)
-
- # prompt == none requires id token hint
- bearer = BearerToken(self.mock_validator)
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=invalid_request', h['Location'])
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- self.request.id_token_hint = 'me@email.com'
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
- self.assertEqual(b, None)
- self.assertEqual(s, 302)
-
- # Test alernative response modes
- self.request.response_mode = 'query'
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertURLEqual(h['Location'], self.url_query)
-
- # Ensure silent authentication and authorization is done
- self.mock_validator.validate_silent_login.return_value = False
- self.mock_validator.validate_silent_authorization.return_value = True
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=login_required', h['Location'])
-
- self.mock_validator.validate_silent_login.return_value = True
- self.mock_validator.validate_silent_authorization.return_value = False
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=consent_required', h['Location'])
-
- # ID token hint must match logged in user
- self.mock_validator.validate_silent_authorization.return_value = True
- self.mock_validator.validate_user_match.return_value = False
- h, b, s = self.auth.create_authorization_response(self.request, bearer)
- self.assertIn('error=login_required', h['Location'])
-
-
-class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest):
-
- def setUp(self):
- super(OpenIDHybridCodeTokenTest, self).setUp()
- self.request.response_type = 'code token'
- self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator)
- self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc'
- self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc'
-
-
-class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest):
-
- def setUp(self):
- super(OpenIDHybridCodeIdTokenTest, self).setUp()
- self.request.response_type = 'code id_token'
- self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator)
- token = 'MOCKED_TOKEN'
- self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token
- self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token
-
-
-class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest):
-
- def setUp(self):
- super(OpenIDHybridCodeIdTokenTokenTest, self).setUp()
- self.request.response_type = 'code id_token token'
- self.auth = OpenIDConnectHybrid(request_validator=self.mock_validator)
- token = 'MOCKED_TOKEN'
- self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
- self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
-
-
-class ImplicitTokenGrantDispatcherTest(TestCase):
- def setUp(self):
- self.request = Request('http://a.b/path')
- request_validator = mock.MagicMock()
- implicit_grant = ImplicitGrant(request_validator)
- openid_connect_implicit = OpenIDConnectImplicit(request_validator)
-
- self.dispatcher = ImplicitTokenGrantDispatcher(
- default_implicit_grant=implicit_grant,
- oidc_implicit_grant=openid_connect_implicit
- )
-
- def test_create_authorization_response_openid(self):
- self.request.scopes = ('hello', 'openid')
- self.request.response_type = 'id_token'
- handler = self.dispatcher._handler_for_request(self.request)
- self.assertTrue(isinstance(handler, OpenIDConnectImplicit))
-
- def test_validate_authorization_request_openid(self):
- self.request.scopes = ('hello', 'openid')
- self.request.response_type = 'id_token'
- handler = self.dispatcher._handler_for_request(self.request)
- self.assertTrue(isinstance(handler, OpenIDConnectImplicit))
-
- def test_create_authorization_response_oauth(self):
- self.request.scopes = ('hello', 'world')
- handler = self.dispatcher._handler_for_request(self.request)
- self.assertTrue(isinstance(handler, ImplicitGrant))
-
- def test_validate_authorization_request_oauth(self):
- self.request.scopes = ('hello', 'world')
- handler = self.dispatcher._handler_for_request(self.request)
- self.assertTrue(isinstance(handler, ImplicitGrant))
-
-
-class DispatcherTest(TestCase):
- def setUp(self):
- self.request = Request('http://a.b/path')
- self.request.decoded_body = (
- ("client_id", "me"),
- ("code", "code"),
- ("redirect_url", "https://a.b/cb"),
- )
-
- self.request_validator = mock.MagicMock()
- self.auth_grant = AuthorizationCodeGrant(self.request_validator)
- self.openid_connect_auth = OpenIDConnectAuthCode(self.request_validator)
-
-
-class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest):
-
- def setUp(self):
- super(AuthTokenGrantDispatcherOpenIdTest, self).setUp()
- self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid')
- self.dispatcher = AuthTokenGrantDispatcher(
- self.request_validator,
- default_token_grant=self.auth_grant,
- oidc_token_grant=self.openid_connect_auth
- )
-
- def test_create_token_response_openid(self):
- handler = self.dispatcher._handler_for_request(self.request)
- self.assertTrue(isinstance(handler, OpenIDConnectAuthCode))
- self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called)
-
-
-class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest):
-
- def setUp(self):
- super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp()
- self.request.decoded_body = (
- ("client_id", "me"),
- ("code", ""),
- ("redirect_url", "https://a.b/cb"),
- )
- self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid')
- self.dispatcher = AuthTokenGrantDispatcher(
- self.request_validator,
- default_token_grant=self.auth_grant,
- oidc_token_grant=self.openid_connect_auth
- )
-
- def test_create_token_response_openid_without_code(self):
- handler = self.dispatcher._handler_for_request(self.request)
- self.assertTrue(isinstance(handler, AuthorizationCodeGrant))
- self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called)
-
-
-class AuthTokenGrantDispatcherOAuthTest(DispatcherTest):
-
- def setUp(self):
- super(AuthTokenGrantDispatcherOAuthTest, self).setUp()
- self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world')
- self.dispatcher = AuthTokenGrantDispatcher(
- self.request_validator,
- default_token_grant=self.auth_grant,
- oidc_token_grant=self.openid_connect_auth
- )
-
- def test_create_token_response_oauth(self):
- handler = self.dispatcher._handler_for_request(self.request)
- self.assertTrue(isinstance(handler, AuthorizationCodeGrant))
- self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called)
diff --git a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
index 21540a2..32a0977 100644
--- a/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
+++ b/tests/oauth2/rfc6749/grant_types/test_refresh_token.py
@@ -99,7 +99,7 @@ class RefreshTokenGrantTest(TestCase):
token = json.loads(body)
self.assertEqual(self.mock_validator.save_token.call_count, 0)
self.assertEqual(token['error'], 'invalid_scope')
- self.assertEqual(status_code, 401)
+ self.assertEqual(status_code, 400)
def test_invalid_token(self):
self.mock_validator.validate_refresh_token.return_value = False
@@ -109,7 +109,7 @@ class RefreshTokenGrantTest(TestCase):
token = json.loads(body)
self.assertEqual(self.mock_validator.save_token.call_count, 0)
self.assertEqual(token['error'], 'invalid_grant')
- self.assertEqual(status_code, 401)
+ self.assertEqual(status_code, 400)
def test_invalid_client(self):
self.mock_validator.authenticate_client.return_value = False
diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py
index 2a9cbe8..c42f516 100644
--- a/tests/oauth2/rfc6749/test_parameters.py
+++ b/tests/oauth2/rfc6749/test_parameters.py
@@ -86,7 +86,7 @@ class ParameterTests(TestCase):
'access_token': '2YotnFZFEjr1zCsicMWpAA',
'state': state,
'token_type': 'example',
- 'expires_in': '3600',
+ 'expires_in': 3600,
'expires_at': 4600,
'scope': ['abc']
}
@@ -103,6 +103,7 @@ class ParameterTests(TestCase):
' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
' "example_parameter": "example_value" }')
+ json_custom_error = '{ "error": "incorrect_client_credentials" }'
json_error = '{ "error": "access_denied" }'
json_notoken = ('{ "token_type": "example",'
@@ -115,13 +116,6 @@ class ParameterTests(TestCase):
' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
' "example_parameter": "example_value" }')
- json_expires = ('{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
- ' "token_type": "example",'
- ' "expires": 3600,'
- ' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
- ' "example_parameter": "example_value",'
- ' "scope":"abc def"}')
-
json_dict = {
'access_token': '2YotnFZFEjr1zCsicMWpAA',
'token_type': 'example',
@@ -204,6 +198,9 @@ class ParameterTests(TestCase):
self.assertRaises(ValueError, parse_implicit_response,
self.implicit_wrongstate, state=self.state)
+ def test_custom_json_error(self):
+ self.assertRaises(CustomOAuth2Error, parse_token_response, self.json_custom_error)
+
def test_json_token_response(self):
"""Verify correct parameter parsing and validation for token responses. """
self.assertEqual(parse_token_response(self.json_response), self.json_dict)
@@ -264,7 +261,3 @@ class ParameterTests(TestCase):
finally:
signals.scope_changed.disconnect(record_scope_change)
del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE']
-
- def test_token_response_with_expires(self):
- """Verify fallback for alternate spelling of expires_in. """
- self.assertEqual(parse_token_response(self.json_expires), self.json_dict)
diff --git a/tests/oauth2/rfc6749/test_server.py b/tests/oauth2/rfc6749/test_server.py
index da303ce..b623a9b 100644
--- a/tests/oauth2/rfc6749/test_server.py
+++ b/tests/oauth2/rfc6749/test_server.py
@@ -3,21 +3,17 @@ from __future__ import absolute_import, unicode_literals
import json
-import jwt
import mock
from oauthlib import common
from oauthlib.oauth2.rfc6749 import errors, tokens
from oauthlib.oauth2.rfc6749.endpoints import Server
-from oauthlib.oauth2.rfc6749.endpoints.authorization import \
- AuthorizationEndpoint
+from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint
from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint
from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint
from oauthlib.oauth2.rfc6749.grant_types import (AuthorizationCodeGrant,
ClientCredentialsGrant,
ImplicitGrant,
- OpenIDConnectAuthCode,
- OpenIDConnectImplicit,
ResourceOwnerPasswordCredentialsGrant)
from ...unittest import TestCase
@@ -27,42 +23,37 @@ class AuthorizationEndpointTest(TestCase):
def setUp(self):
self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_code_challenge.return_value = None
self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
auth_code = AuthorizationCodeGrant(
- request_validator=self.mock_validator)
+ request_validator=self.mock_validator)
auth_code.save_authorization_code = mock.MagicMock()
implicit = ImplicitGrant(
- request_validator=self.mock_validator)
+ request_validator=self.mock_validator)
implicit.save_token = mock.MagicMock()
- openid_connect_auth = OpenIDConnectAuthCode(self.mock_validator)
- openid_connect_implicit = OpenIDConnectImplicit(self.mock_validator)
-
response_types = {
- 'code': auth_code,
- 'token': implicit,
-
- 'id_token': openid_connect_implicit,
- 'id_token token': openid_connect_implicit,
- 'code token': openid_connect_auth,
- 'code id_token': openid_connect_auth,
- 'code token id_token': openid_connect_auth,
- 'none': auth_code
+ 'code': auth_code,
+ 'token': implicit,
+ 'none': auth_code
}
self.expires_in = 1800
- token = tokens.BearerToken(self.mock_validator,
- expires_in=self.expires_in)
+ token = tokens.BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
self.endpoint = AuthorizationEndpoint(
- default_response_type='code',
- default_token_type=token,
- response_types=response_types)
+ default_response_type='code',
+ default_token_type=token,
+ response_types=response_types
+ )
@mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
def test_authorization_grant(self):
uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz'
uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
headers, body, status_code = self.endpoint.create_authorization_response(
- uri, scopes=['all', 'of', 'them'])
+ uri, scopes=['all', 'of', 'them'])
self.assertIn('Location', headers)
self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz')
@@ -71,7 +62,7 @@ class AuthorizationEndpointTest(TestCase):
uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz'
uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
headers, body, status_code = self.endpoint.create_authorization_response(
- uri, scopes=['all', 'of', 'them'])
+ uri, scopes=['all', 'of', 'them'])
self.assertIn('Location', headers)
self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True)
@@ -79,7 +70,7 @@ class AuthorizationEndpointTest(TestCase):
uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz'
uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
headers, body, status_code = self.endpoint.create_authorization_response(
- uri, scopes=['all', 'of', 'them'])
+ uri, scopes=['all', 'of', 'them'])
self.assertIn('Location', headers)
self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True)
self.assertEqual(body, None)
@@ -99,9 +90,9 @@ class AuthorizationEndpointTest(TestCase):
uri = 'http://i.b/l?client_id=me&scope=all+of+them'
uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
self.mock_validator.validate_request = mock.MagicMock(
- side_effect=errors.InvalidRequestError())
+ side_effect=errors.InvalidRequestError())
headers, body, status_code = self.endpoint.create_authorization_response(
- uri, scopes=['all', 'of', 'them'])
+ uri, scopes=['all', 'of', 'them'])
self.assertIn('Location', headers)
self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.')
@@ -109,9 +100,9 @@ class AuthorizationEndpointTest(TestCase):
uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them'
uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
self.mock_validator.validate_request = mock.MagicMock(
- side_effect=errors.UnsupportedResponseTypeError())
+ side_effect=errors.UnsupportedResponseTypeError())
headers, body, status_code = self.endpoint.create_authorization_response(
- uri, scopes=['all', 'of', 'them'])
+ uri, scopes=['all', 'of', 'them'])
self.assertIn('Location', headers)
self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type')
@@ -127,29 +118,35 @@ class TokenEndpointTest(TestCase):
self.mock_validator = mock.MagicMock()
self.mock_validator.authenticate_client.side_effect = set_user
+ self.mock_validator.get_code_challenge.return_value = None
self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
auth_code = AuthorizationCodeGrant(
- request_validator=self.mock_validator)
+ request_validator=self.mock_validator)
password = ResourceOwnerPasswordCredentialsGrant(
- request_validator=self.mock_validator)
+ request_validator=self.mock_validator)
client = ClientCredentialsGrant(
- request_validator=self.mock_validator)
+ request_validator=self.mock_validator)
supported_types = {
- 'authorization_code': auth_code,
- 'password': password,
- 'client_credentials': client,
+ 'authorization_code': auth_code,
+ 'password': password,
+ 'client_credentials': client,
}
self.expires_in = 1800
- token = tokens.BearerToken(self.mock_validator,
- expires_in=self.expires_in)
- self.endpoint = TokenEndpoint('authorization_code',
- default_token_type=token, grant_types=supported_types)
+ token = tokens.BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
+ self.endpoint = TokenEndpoint(
+ 'authorization_code',
+ default_token_type=token,
+ grant_types=supported_types
+ )
@mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
def test_authorization_grant(self):
body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
token = {
'token_type': 'Bearer',
'expires_in': self.expires_in,
@@ -176,7 +173,7 @@ class TokenEndpointTest(TestCase):
def test_password_grant(self):
body = 'grant_type=password&username=a&password=hello&scope=all+of+them'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
token = {
'token_type': 'Bearer',
'expires_in': self.expires_in,
@@ -190,7 +187,7 @@ class TokenEndpointTest(TestCase):
def test_client_grant(self):
body = 'grant_type=client_credentials&scope=all+of+them'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
token = {
'token_type': 'Bearer',
'expires_in': self.expires_in,
@@ -223,6 +220,7 @@ class SignedTokenEndpointTest(TestCase):
return True
self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_code_challenge.return_value = None
self.mock_validator.authenticate_client.side_effect = set_user
self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
@@ -281,7 +279,7 @@ twIDAQAB
def test_authorization_grant(self):
body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
body = json.loads(body)
token = {
'token_type': 'Bearer',
@@ -295,7 +293,7 @@ twIDAQAB
body = 'client_id=me&redirect_uri=http%3A%2F%2Fback.to%2Fme&grant_type=authorization_code&code=abc&state=xyz'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
body = json.loads(body)
token = {
'token_type': 'Bearer',
@@ -310,7 +308,7 @@ twIDAQAB
def test_password_grant(self):
body = 'grant_type=password&username=a&password=hello&scope=all+of+them'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
body = json.loads(body)
token = {
'token_type': 'Bearer',
@@ -325,7 +323,7 @@ twIDAQAB
def test_scopes_and_user_id_stored_in_access_token(self):
body = 'grant_type=password&username=a&password=hello&scope=all+of+them'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
access_token = json.loads(body)['access_token']
@@ -338,7 +336,7 @@ twIDAQAB
def test_client_grant(self):
body = 'grant_type=client_credentials&scope=all+of+them'
headers, body, status_code = self.endpoint.create_token_response(
- '', body=body)
+ '', body=body)
body = json.loads(body)
token = {
'token_type': 'Bearer',
@@ -366,8 +364,10 @@ class ResourceEndpointTest(TestCase):
self.mock_validator = mock.MagicMock()
self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
token = tokens.BearerToken(request_validator=self.mock_validator)
- self.endpoint = ResourceEndpoint(default_token='Bearer',
- token_types={'Bearer': token})
+ self.endpoint = ResourceEndpoint(
+ default_token='Bearer',
+ token_types={'Bearer': token}
+ )
def test_defaults(self):
uri = 'http://a.b/path?some=query'
diff --git a/tests/oauth2/rfc6749/test_tokens.py b/tests/oauth2/rfc6749/test_tokens.py
index e2e558d..061754f 100644
--- a/tests/oauth2/rfc6749/test_tokens.py
+++ b/tests/oauth2/rfc6749/test_tokens.py
@@ -1,6 +1,11 @@
from __future__ import absolute_import, unicode_literals
-from oauthlib.oauth2.rfc6749.tokens import *
+from oauthlib.oauth2.rfc6749.tokens import (
+ prepare_mac_header,
+ prepare_bearer_headers,
+ prepare_bearer_body,
+ prepare_bearer_uri,
+)
from ...unittest import TestCase
@@ -59,9 +64,22 @@ class TokenTest(TestCase):
bearer_headers = {
'Authorization': 'Bearer vF9dft4qmT'
}
+ fake_bearer_headers = [
+ {'Authorization': 'Beaver vF9dft4qmT'},
+ {'Authorization': 'BeavervF9dft4qmT'},
+ {'Authorization': 'Beaver vF9dft4qmT'},
+ {'Authorization': 'BearerF9dft4qmT'},
+ {'Authorization': 'Bearer vF9d ft4qmT'},
+ ]
+ valid_header_with_multiple_spaces = {'Authorization': 'Bearer vF9dft4qmT'}
bearer_body = 'access_token=vF9dft4qmT'
bearer_uri = 'http://server.example.com/resource?access_token=vF9dft4qmT'
+ def _mocked_validate_bearer_token(self, token, scopes, request):
+ if not token:
+ return False
+ return True
+
def test_prepare_mac_header(self):
"""Verify mac signatures correctness
diff --git a/tests/openid/__init__.py b/tests/openid/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/openid/__init__.py
diff --git a/tests/openid/connect/__init__.py b/tests/openid/connect/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/openid/connect/__init__.py
diff --git a/tests/openid/connect/core/__init__.py b/tests/openid/connect/core/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/openid/connect/core/__init__.py
diff --git a/tests/openid/connect/core/endpoints/__init__.py b/tests/openid/connect/core/endpoints/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/openid/connect/core/endpoints/__init__.py
diff --git a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py b/tests/openid/connect/core/endpoints/test_claims_handling.py
index ff72673..270ef69 100644
--- a/tests/oauth2/rfc6749/endpoints/test_claims_handling.py
+++ b/tests/openid/connect/core/endpoints/test_claims_handling.py
@@ -10,10 +10,11 @@ from __future__ import absolute_import, unicode_literals
import mock
-from oauthlib.oauth2 import InvalidRequestError, RequestValidator, Server
+from oauthlib.oauth2 import RequestValidator
+from oauthlib.openid.connect.core.endpoints.pre_configured import Server
-from ....unittest import TestCase
-from .test_utils import get_fragment_credentials, get_query_credentials
+from tests.unittest import TestCase
+from tests.oauth2.rfc6749.endpoints.test_utils import get_query_credentials
class TestClaimsHandling(TestCase):
@@ -55,6 +56,7 @@ class TestClaimsHandling(TestCase):
def setUp(self):
self.validator = mock.MagicMock(spec=RequestValidator)
+ self.validator.get_code_challenge.return_value = None
self.validator.get_default_redirect_uri.return_value = TestClaimsHandling.DEFAULT_REDIRECT_URI
self.validator.authenticate_client.side_effect = self.set_client
@@ -81,7 +83,7 @@ class TestClaimsHandling(TestCase):
}
}
- claims_urlquoted='%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D'
+ claims_urlquoted = '%7B%22id_token%22%3A%20%7B%22claim_2%22%3A%20%7B%22essential%22%3A%20true%7D%2C%20%22claim_1%22%3A%20null%7D%2C%20%22userinfo%22%3A%20%7B%22claim_4%22%3A%20null%2C%20%22claim_3%22%3A%20%7B%22essential%22%3A%20true%7D%7D%7D'
uri = 'http://example.com/path?client_id=abc&scope=openid+test_scope&response_type=code&claims=%s'
h, b, s = self.server.create_authorization_response(uri % claims_urlquoted, scopes='openid test_scope')
@@ -90,8 +92,10 @@ class TestClaimsHandling(TestCase):
code = get_query_credentials(h['Location'])['code'][0]
token_uri = 'http://example.com/path'
- _, body, _ = self.server.create_token_response(token_uri,
- body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code)
+ _, body, _ = self.server.create_token_response(
+ token_uri,
+ body='client_id=me&redirect_uri=http://back.to/me&grant_type=authorization_code&code=%s' % code
+ )
self.assertDictEqual(self.claims_saved_with_bearer_token, claims)
diff --git a/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py
index 89431b6..517239a 100644
--- a/tests/oauth2/rfc6749/endpoints/test_openid_connect_params_handling.py
+++ b/tests/openid/connect/core/endpoints/test_openid_connect_params_handling.py
@@ -5,10 +5,10 @@ import mock
from oauthlib.oauth2 import InvalidRequestError
from oauthlib.oauth2.rfc6749.endpoints.authorization import \
AuthorizationEndpoint
-from oauthlib.oauth2.rfc6749.grant_types import OpenIDConnectAuthCode
from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types import AuthorizationCodeGrant
-from ....unittest import TestCase
+from tests.unittest import TestCase
try:
from urllib.parse import urlencode
@@ -16,14 +16,12 @@ except ImportError:
from urllib import urlencode
-
-
class OpenIDConnectEndpointTest(TestCase):
def setUp(self):
self.mock_validator = mock.MagicMock()
self.mock_validator.authenticate_client.side_effect = self.set_client
- grant = OpenIDConnectAuthCode(request_validator=self.mock_validator)
+ grant = AuthorizationCodeGrant(request_validator=self.mock_validator)
bearer = BearerToken(self.mock_validator)
self.endpoint = AuthorizationEndpoint(grant, bearer,
response_types={'code': grant})
diff --git a/tests/openid/connect/core/grant_types/__init__.py b/tests/openid/connect/core/grant_types/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/openid/connect/core/grant_types/__init__.py
diff --git a/tests/openid/connect/core/grant_types/test_authorization_code.py b/tests/openid/connect/core/grant_types/test_authorization_code.py
new file mode 100644
index 0000000..c3c7824
--- /dev/null
+++ b/tests/openid/connect/core/grant_types/test_authorization_code.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+import json
+
+import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant
+from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt
+
+from tests.unittest import TestCase
+from tests.oauth2.rfc6749.grant_types.test_authorization_code import \
+ AuthorizationCodeGrantTest
+
+
+def get_id_token_mock(token, token_handler, request):
+ return "MOCKED_TOKEN"
+
+
+class OpenIDAuthCodeInterferenceTest(AuthorizationCodeGrantTest):
+ """Test that OpenID don't interfere with normal OAuth 2 flows."""
+
+ def setUp(self):
+ super(OpenIDAuthCodeInterferenceTest, self).setUp()
+ self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator)
+
+
+class OpenIDAuthCodeTest(TestCase):
+
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.scopes = ('hello', 'openid')
+ self.request.expires_in = 1800
+ self.request.client_id = 'abcdef'
+ self.request.code = '1234'
+ self.request.response_type = 'code'
+ self.request.grant_type = 'authorization_code'
+ self.request.redirect_uri = 'https://a.b/cb'
+ self.request.state = 'abc'
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.authenticate_client.side_effect = self.set_client
+ self.mock_validator.get_code_challenge.return_value = None
+ self.mock_validator.get_id_token.side_effect = get_id_token_mock
+ self.auth = AuthorizationCodeGrant(request_validator=self.mock_validator)
+
+ self.url_query = 'https://a.b/cb?code=abc&state=abc'
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc'
+
+ def set_client(self, request):
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked'
+ return True
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_authorization(self, generate_token):
+
+ scope, info = self.auth.validate_authorization_request(self.request)
+
+ generate_token.return_value = 'abc'
+ bearer = BearerToken(self.mock_validator)
+ self.request.response_mode = 'query'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_query)
+ self.assertEqual(b, None)
+ self.assertEqual(s, 302)
+
+ self.request.response_mode = 'fragment'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+ self.assertEqual(b, None)
+ self.assertEqual(s, 302)
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_no_prompt_authorization(self, generate_token):
+ generate_token.return_value = 'abc'
+ scope, info = self.auth.validate_authorization_request(self.request)
+ self.request.prompt = 'none'
+ self.assertRaises(OIDCNoPrompt,
+ self.auth.validate_authorization_request,
+ self.request)
+
+ bearer = BearerToken(self.mock_validator)
+
+ self.request.response_mode = 'query'
+ self.request.id_token_hint = 'me@email.com'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_query)
+ self.assertEqual(b, None)
+ self.assertEqual(s, 302)
+
+ # Test alernative response modes
+ self.request.response_mode = 'fragment'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+
+ # Ensure silent authentication and authorization is done
+ self.mock_validator.validate_silent_login.return_value = False
+ self.mock_validator.validate_silent_authorization.return_value = True
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+ self.mock_validator.validate_silent_login.return_value = True
+ self.mock_validator.validate_silent_authorization.return_value = False
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=consent_required', h['Location'])
+
+ # ID token hint must match logged in user
+ self.mock_validator.validate_silent_authorization.return_value = True
+ self.mock_validator.validate_user_match.return_value = False
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+ def set_scopes(self, client_id, code, client, request):
+ request.scopes = self.request.scopes
+ request.state = self.request.state
+ request.user = 'bob'
+ return True
+
+ def test_create_token_response(self):
+ self.request.response_type = None
+ self.mock_validator.validate_code.side_effect = self.set_scopes
+
+ bearer = BearerToken(self.mock_validator)
+
+ h, token, s = self.auth.create_token_response(self.request, bearer)
+ token = json.loads(token)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('scope', token)
+ self.assertIn('id_token', token)
+ self.assertIn('openid', token['scope'])
+
+ self.mock_validator.reset_mock()
+
+ self.request.scopes = ('hello', 'world')
+ h, token, s = self.auth.create_token_response(self.request, bearer)
+ token = json.loads(token)
+ self.assertEqual(self.mock_validator.save_token.call_count, 1)
+ self.assertIn('access_token', token)
+ self.assertIn('refresh_token', token)
+ self.assertIn('expires_in', token)
+ self.assertIn('scope', token)
+ self.assertNotIn('id_token', token)
+ self.assertNotIn('openid', token['scope'])
diff --git a/tests/openid/connect/core/grant_types/test_dispatchers.py b/tests/openid/connect/core/grant_types/test_dispatchers.py
new file mode 100644
index 0000000..9e45d65
--- /dev/null
+++ b/tests/openid/connect/core/grant_types/test_dispatchers.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+import mock
+
+from oauthlib.common import Request
+
+from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant
+from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant
+from oauthlib.openid.connect.core.grant_types.dispatchers import (
+ ImplicitTokenGrantDispatcher,
+ AuthorizationTokenGrantDispatcher
+)
+
+from oauthlib.oauth2.rfc6749.grant_types import (
+ AuthorizationCodeGrant as OAuth2AuthorizationCodeGrant,
+ ImplicitGrant as OAuth2ImplicitGrant,
+)
+
+
+from tests.unittest import TestCase
+
+
+class ImplicitTokenGrantDispatcherTest(TestCase):
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ request_validator = mock.MagicMock()
+ implicit_grant = OAuth2ImplicitGrant(request_validator)
+ openid_connect_implicit = ImplicitGrant(request_validator)
+
+ self.dispatcher = ImplicitTokenGrantDispatcher(
+ default_grant=implicit_grant,
+ oidc_grant=openid_connect_implicit
+ )
+
+ def test_create_authorization_response_openid(self):
+ self.request.scopes = ('hello', 'openid')
+ self.request.response_type = 'id_token'
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, ImplicitGrant)
+
+ def test_validate_authorization_request_openid(self):
+ self.request.scopes = ('hello', 'openid')
+ self.request.response_type = 'id_token'
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, ImplicitGrant)
+
+ def test_create_authorization_response_oauth(self):
+ self.request.scopes = ('hello', 'world')
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2ImplicitGrant)
+
+ def test_validate_authorization_request_oauth(self):
+ self.request.scopes = ('hello', 'world')
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2ImplicitGrant)
+
+
+class DispatcherTest(TestCase):
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.decoded_body = (
+ ("client_id", "me"),
+ ("code", "code"),
+ ("redirect_url", "https://a.b/cb"),
+ )
+
+ self.request_validator = mock.MagicMock()
+ self.auth_grant = OAuth2AuthorizationCodeGrant(self.request_validator)
+ self.openid_connect_auth = AuthorizationCodeGrant(self.request_validator)
+
+
+class AuthTokenGrantDispatcherOpenIdTest(DispatcherTest):
+
+ def setUp(self):
+ super(AuthTokenGrantDispatcherOpenIdTest, self).setUp()
+ self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid')
+ self.dispatcher = AuthorizationTokenGrantDispatcher(
+ self.request_validator,
+ default_grant=self.auth_grant,
+ oidc_grant=self.openid_connect_auth
+ )
+
+ def test_create_token_response_openid(self):
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, AuthorizationCodeGrant)
+ self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called)
+
+
+class AuthTokenGrantDispatcherOpenIdWithoutCodeTest(DispatcherTest):
+
+ def setUp(self):
+ super(AuthTokenGrantDispatcherOpenIdWithoutCodeTest, self).setUp()
+ self.request.decoded_body = (
+ ("client_id", "me"),
+ ("code", ""),
+ ("redirect_url", "https://a.b/cb"),
+ )
+ self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'openid')
+ self.dispatcher = AuthorizationTokenGrantDispatcher(
+ self.request_validator,
+ default_grant=self.auth_grant,
+ oidc_grant=self.openid_connect_auth
+ )
+
+ def test_create_token_response_openid_without_code(self):
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2AuthorizationCodeGrant)
+ self.assertFalse(self.dispatcher.request_validator.get_authorization_code_scopes.called)
+
+
+class AuthTokenGrantDispatcherOAuthTest(DispatcherTest):
+
+ def setUp(self):
+ super(AuthTokenGrantDispatcherOAuthTest, self).setUp()
+ self.request_validator.get_authorization_code_scopes.return_value = ('hello', 'world')
+ self.dispatcher = AuthorizationTokenGrantDispatcher(
+ self.request_validator,
+ default_grant=self.auth_grant,
+ oidc_grant=self.openid_connect_auth
+ )
+
+ def test_create_token_response_oauth(self):
+ handler = self.dispatcher._handler_for_request(self.request)
+ self.assertIsInstance(handler, OAuth2AuthorizationCodeGrant)
+ self.assertTrue(self.dispatcher.request_validator.get_authorization_code_scopes.called)
diff --git a/tests/openid/connect/core/grant_types/test_hybrid.py b/tests/openid/connect/core/grant_types/test_hybrid.py
new file mode 100644
index 0000000..6eb8037
--- /dev/null
+++ b/tests/openid/connect/core/grant_types/test_hybrid.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant
+
+from tests.oauth2.rfc6749.grant_types.test_authorization_code import \
+ AuthorizationCodeGrantTest
+
+
+class OpenIDHybridInterferenceTest(AuthorizationCodeGrantTest):
+ """Test that OpenID don't interfere with normal OAuth 2 flows."""
+
+ def setUp(self):
+ super(OpenIDHybridInterferenceTest, self).setUp()
+ self.auth = HybridGrant(request_validator=self.mock_validator)
diff --git a/tests/openid/connect/core/grant_types/test_implicit.py b/tests/openid/connect/core/grant_types/test_implicit.py
new file mode 100644
index 0000000..7ab198a
--- /dev/null
+++ b/tests/openid/connect/core/grant_types/test_implicit.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+import mock
+
+from oauthlib.common import Request
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+from oauthlib.openid.connect.core.grant_types.exceptions import OIDCNoPrompt
+from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant
+from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant
+from tests.oauth2.rfc6749.grant_types.test_implicit import ImplicitGrantTest
+from tests.unittest import TestCase
+from .test_authorization_code import get_id_token_mock, OpenIDAuthCodeTest
+
+
+class OpenIDImplicitInterferenceTest(ImplicitGrantTest):
+ """Test that OpenID don't interfere with normal OAuth 2 flows."""
+
+ def setUp(self):
+ super(OpenIDImplicitInterferenceTest, self).setUp()
+ self.auth = ImplicitGrant(request_validator=self.mock_validator)
+
+
+class OpenIDImplicitTest(TestCase):
+
+ def setUp(self):
+ self.request = Request('http://a.b/path')
+ self.request.scopes = ('hello', 'openid')
+ self.request.expires_in = 1800
+ self.request.client_id = 'abcdef'
+ self.request.response_type = 'id_token token'
+ self.request.redirect_uri = 'https://a.b/cb'
+ self.request.nonce = 'zxc'
+ self.request.state = 'abc'
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_id_token.side_effect = get_id_token_mock
+ self.auth = ImplicitGrant(request_validator=self.mock_validator)
+
+ token = 'MOCKED_TOKEN'
+ self.url_query = 'https://a.b/cb?state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
+ self.url_fragment = 'https://a.b/cb#state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_authorization(self, generate_token):
+ scope, info = self.auth.validate_authorization_request(self.request)
+
+ generate_token.return_value = 'abc'
+ bearer = BearerToken(self.mock_validator)
+
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+ self.assertEqual(b, None)
+ self.assertEqual(s, 302)
+
+ self.request.response_type = 'id_token'
+ token = 'MOCKED_TOKEN'
+ url = 'https://a.b/cb#state=abc&id_token=%s' % token
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], url, parse_fragment=True)
+ self.assertEqual(b, None)
+ self.assertEqual(s, 302)
+
+ self.request.nonce = None
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=invalid_request', h['Location'])
+ self.assertEqual(b, None)
+ self.assertEqual(s, 302)
+
+ @mock.patch('oauthlib.common.generate_token')
+ def test_no_prompt_authorization(self, generate_token):
+ generate_token.return_value = 'abc'
+ scope, info = self.auth.validate_authorization_request(self.request)
+ self.request.prompt = 'none'
+ self.assertRaises(OIDCNoPrompt,
+ self.auth.validate_authorization_request,
+ self.request)
+
+ bearer = BearerToken(self.mock_validator)
+ self.request.id_token_hint = 'me@email.com'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_fragment, parse_fragment=True)
+ self.assertEqual(b, None)
+ self.assertEqual(s, 302)
+
+ # Test alernative response modes
+ self.request.response_mode = 'query'
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertURLEqual(h['Location'], self.url_query)
+
+ # Ensure silent authentication and authorization is done
+ self.mock_validator.validate_silent_login.return_value = False
+ self.mock_validator.validate_silent_authorization.return_value = True
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+ self.mock_validator.validate_silent_login.return_value = True
+ self.mock_validator.validate_silent_authorization.return_value = False
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=consent_required', h['Location'])
+
+ # ID token hint must match logged in user
+ self.mock_validator.validate_silent_authorization.return_value = True
+ self.mock_validator.validate_user_match.return_value = False
+ h, b, s = self.auth.create_authorization_response(self.request, bearer)
+ self.assertIn('error=login_required', h['Location'])
+
+
+class OpenIDHybridCodeTokenTest(OpenIDAuthCodeTest):
+
+ def setUp(self):
+ super(OpenIDHybridCodeTokenTest, self).setUp()
+ self.request.response_type = 'code token'
+ self.auth = HybridGrant(request_validator=self.mock_validator)
+ self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc'
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc'
+
+
+class OpenIDHybridCodeIdTokenTest(OpenIDAuthCodeTest):
+
+ def setUp(self):
+ super(OpenIDHybridCodeIdTokenTest, self).setUp()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.request.response_type = 'code id_token'
+ self.auth = HybridGrant(request_validator=self.mock_validator)
+ token = 'MOCKED_TOKEN'
+ self.url_query = 'https://a.b/cb?code=abc&state=abc&id_token=%s' % token
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc&id_token=%s' % token
+
+
+class OpenIDHybridCodeIdTokenTokenTest(OpenIDAuthCodeTest):
+
+ def setUp(self):
+ super(OpenIDHybridCodeIdTokenTokenTest, self).setUp()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.request.response_type = 'code id_token token'
+ self.auth = HybridGrant(request_validator=self.mock_validator)
+ token = 'MOCKED_TOKEN'
+ self.url_query = 'https://a.b/cb?code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
+ self.url_fragment = 'https://a.b/cb#code=abc&state=abc&token_type=Bearer&expires_in=3600&scope=hello+openid&access_token=abc&id_token=%s' % token
diff --git a/tests/openid/connect/core/test_request_validator.py b/tests/openid/connect/core/test_request_validator.py
new file mode 100644
index 0000000..1e71fb1
--- /dev/null
+++ b/tests/openid/connect/core/test_request_validator.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+from oauthlib.openid.connect.core.request_validator import RequestValidator
+
+from tests.unittest import TestCase
+
+
+class RequestValidatorTest(TestCase):
+
+ def test_method_contracts(self):
+ v = RequestValidator()
+ self.assertRaises(
+ NotImplementedError,
+ v.get_authorization_code_scopes,
+ 'client_id', 'code', 'redirect_uri', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.get_jwt_bearer_token,
+ 'token', 'token_handler', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.get_id_token,
+ 'token', 'token_handler', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_jwt_bearer_token,
+ 'token', 'scopes', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_id_token,
+ 'token', 'scopes', 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_silent_authorization,
+ 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_silent_login,
+ 'request'
+ )
+ self.assertRaises(
+ NotImplementedError,
+ v.validate_user_match,
+ 'id_token_hint', 'scopes', 'claims', 'request'
+ )
diff --git a/tests/openid/connect/core/test_server.py b/tests/openid/connect/core/test_server.py
new file mode 100644
index 0000000..ffab7b0
--- /dev/null
+++ b/tests/openid/connect/core/test_server.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+import json
+
+import mock
+
+from oauthlib.oauth2.rfc6749 import errors
+from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint
+from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint
+from oauthlib.oauth2.rfc6749.tokens import BearerToken
+
+from oauthlib.openid.connect.core.grant_types.authorization_code import AuthorizationCodeGrant
+from oauthlib.openid.connect.core.grant_types.implicit import ImplicitGrant
+from oauthlib.openid.connect.core.grant_types.hybrid import HybridGrant
+
+from tests.unittest import TestCase
+
+
+class AuthorizationEndpointTest(TestCase):
+
+ def setUp(self):
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.get_code_challenge.return_value = None
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+ auth_code = AuthorizationCodeGrant(request_validator=self.mock_validator)
+ auth_code.save_authorization_code = mock.MagicMock()
+ implicit = ImplicitGrant(
+ request_validator=self.mock_validator)
+ implicit.save_token = mock.MagicMock()
+ hybrid = HybridGrant(self.mock_validator)
+
+ response_types = {
+ 'code': auth_code,
+ 'token': implicit,
+ 'id_token': implicit,
+ 'id_token token': implicit,
+ 'code token': hybrid,
+ 'code id_token': hybrid,
+ 'code token id_token': hybrid,
+ 'none': auth_code
+ }
+ self.expires_in = 1800
+ token = BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
+ self.endpoint = AuthorizationEndpoint(
+ default_response_type='code',
+ default_token_type=token,
+ response_types=response_types
+ )
+
+ # TODO: Add hybrid grant test
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_authorization_grant(self):
+ uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?code=abc&state=xyz')
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_implicit_grant(self):
+ uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me#access_token=abc&expires_in=' + str(self.expires_in) + '&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True)
+
+ def test_none_grant(self):
+ uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them&state=xyz'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?state=xyz', parse_fragment=True)
+ self.assertEqual(body, None)
+ self.assertEqual(status_code, 302)
+
+ # and without the state parameter
+ uri = 'http://i.b/l?response_type=none&client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me', parse_fragment=True)
+ self.assertEqual(body, None)
+ self.assertEqual(status_code, 302)
+
+ def test_missing_type(self):
+ uri = 'http://i.b/l?client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ self.mock_validator.validate_request = mock.MagicMock(
+ side_effect=errors.InvalidRequestError())
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.')
+
+ def test_invalid_type(self):
+ uri = 'http://i.b/l?response_type=invalid&client_id=me&scope=all+of+them'
+ uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme'
+ self.mock_validator.validate_request = mock.MagicMock(
+ side_effect=errors.UnsupportedResponseTypeError())
+ headers, body, status_code = self.endpoint.create_authorization_response(
+ uri, scopes=['all', 'of', 'them'])
+ self.assertIn('Location', headers)
+ self.assertURLEqual(headers['Location'], 'http://back.to/me?error=unsupported_response_type')
+
+
+class TokenEndpointTest(TestCase):
+
+ def setUp(self):
+ def set_user(request):
+ request.user = mock.MagicMock()
+ request.client = mock.MagicMock()
+ request.client.client_id = 'mocked_client_id'
+ return True
+
+ self.mock_validator = mock.MagicMock()
+ self.mock_validator.authenticate_client.side_effect = set_user
+ self.mock_validator.get_code_challenge.return_value = None
+ self.addCleanup(setattr, self, 'mock_validator', mock.MagicMock())
+ auth_code = AuthorizationCodeGrant(
+ request_validator=self.mock_validator)
+ supported_types = {
+ 'authorization_code': auth_code,
+ }
+ self.expires_in = 1800
+ token = BearerToken(
+ self.mock_validator,
+ expires_in=self.expires_in
+ )
+ self.endpoint = TokenEndpoint(
+ 'authorization_code',
+ default_token_type=token,
+ grant_types=supported_types
+ )
+
+ @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc')
+ def test_authorization_grant(self):
+ body = 'grant_type=authorization_code&code=abc&scope=all+of+them&state=xyz'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'refresh_token': 'abc',
+ 'scope': 'all of them',
+ 'state': 'xyz'
+ }
+ self.assertEqual(json.loads(body), token)
+
+ body = 'grant_type=authorization_code&code=abc&state=xyz'
+ headers, body, status_code = self.endpoint.create_token_response(
+ '', body=body)
+ token = {
+ 'token_type': 'Bearer',
+ 'expires_in': self.expires_in,
+ 'access_token': 'abc',
+ 'refresh_token': 'abc',
+ 'state': 'xyz'
+ }
+ self.assertEqual(json.loads(body), token)
+
+ def test_missing_type(self):
+ _, body, _ = self.endpoint.create_token_response('', body='')
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
+
+ def test_invalid_type(self):
+ body = 'grant_type=invalid'
+ _, body, _ = self.endpoint.create_token_response('', body=body)
+ token = {'error': 'unsupported_grant_type'}
+ self.assertEqual(json.loads(body), token)
diff --git a/tests/openid/connect/core/test_tokens.py b/tests/openid/connect/core/test_tokens.py
new file mode 100644
index 0000000..1fcfb51
--- /dev/null
+++ b/tests/openid/connect/core/test_tokens.py
@@ -0,0 +1,133 @@
+from __future__ import absolute_import, unicode_literals
+
+import mock
+
+from oauthlib.openid.connect.core.tokens import JWTToken
+
+from tests.unittest import TestCase
+
+
+class JWTTokenTestCase(TestCase):
+
+ def test_create_token_callable_expires_in(self):
+ """
+ Test retrieval of the expires in value by calling the callable expires_in property
+ """
+
+ expires_in_mock = mock.MagicMock()
+ request_mock = mock.MagicMock()
+
+ token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock())
+ token.create_token(request=request_mock)
+
+ expires_in_mock.assert_called_once_with(request_mock)
+
+ def test_create_token_non_callable_expires_in(self):
+ """
+ When a non callable expires in is set this should just be set to the request
+ """
+
+ expires_in_mock = mock.NonCallableMagicMock()
+ request_mock = mock.MagicMock()
+
+ token = JWTToken(expires_in=expires_in_mock, request_validator=mock.MagicMock())
+ token.create_token(request=request_mock)
+
+ self.assertFalse(expires_in_mock.called)
+ self.assertEqual(request_mock.expires_in, expires_in_mock)
+
+ def test_create_token_calls_get_id_token(self):
+ """
+ When create_token is called the call should be forwarded to the get_id_token on the token validator
+ """
+ request_mock = mock.MagicMock()
+
+ with mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator',
+ autospec=True) as RequestValidatorMock:
+
+ request_validator = RequestValidatorMock()
+
+ token = JWTToken(expires_in=mock.MagicMock(), request_validator=request_validator)
+ token.create_token(request=request_mock)
+
+ request_validator.get_jwt_bearer_token.assert_called_once_with(None, None, request_mock)
+
+ def test_validate_request_token_from_headers(self):
+ """
+ Bearer token get retrieved from headers.
+ """
+
+ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \
+ mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator',
+ autospec=True) as RequestValidatorMock:
+ request_validator_mock = RequestValidatorMock()
+
+ token = JWTToken(request_validator=request_validator_mock)
+
+ request = RequestMock('/uri')
+ # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch
+ # with autospec=True
+ request.scopes = mock.MagicMock()
+ request.headers = {
+ 'Authorization': 'Bearer some-token-from-header'
+ }
+
+ token.validate_request(request=request)
+
+ request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-header',
+ request.scopes,
+ request)
+
+ def test_validate_token_from_request(self):
+ """
+ Token get retrieved from request object.
+ """
+
+ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock, \
+ mock.patch('oauthlib.oauth2.rfc6749.request_validator.RequestValidator',
+ autospec=True) as RequestValidatorMock:
+ request_validator_mock = RequestValidatorMock()
+
+ token = JWTToken(request_validator=request_validator_mock)
+
+ request = RequestMock('/uri')
+ # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch
+ # with autospec=True
+ request.scopes = mock.MagicMock()
+ request.access_token = 'some-token-from-request-object'
+ request.headers = {}
+
+ token.validate_request(request=request)
+
+ request_validator_mock.validate_jwt_bearer_token.assert_called_once_with('some-token-from-request-object',
+ request.scopes,
+ request)
+
+ def test_estimate_type(self):
+ """
+ Estimate type results for a jwt token
+ """
+
+ def test_token(token, expected_result):
+ with mock.patch('oauthlib.common.Request', autospec=True) as RequestMock:
+ jwt_token = JWTToken()
+
+ request = RequestMock('/uri')
+ # Scopes is retrieved using the __call__ method which is not picked up correctly by mock.patch
+ # with autospec=True
+ request.headers = {
+ 'Authorization': 'Bearer {}'.format(token)
+ }
+
+ result = jwt_token.estimate_type(request=request)
+
+ self.assertEqual(result, expected_result)
+
+ test_items = (
+ ('eyfoo.foo.foo', 10),
+ ('eyfoo.foo.foo.foo.foo', 10),
+ ('eyfoobar', 0)
+ )
+
+ for token, expected_result in test_items:
+ test_token(token, expected_result)
diff --git a/tests/test_common.py b/tests/test_common.py
index b0ea20d..20d9f5b 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -10,11 +10,6 @@ from oauthlib.common import (CaseInsensitiveDict, Request, add_params_to_uri,
from .unittest import TestCase
-if sys.version_info[0] == 3:
- bytes_type = bytes
-else:
- bytes_type = lambda s, e: str(s)
-
PARAMS_DICT = {'foo': 'bar', 'baz': '123', }
PARAMS_TWOTUPLE = [('foo', 'bar'), ('baz', '123')]
PARAMS_FORMENCODED = 'foo=bar&baz=123'
@@ -39,6 +34,8 @@ class EncodingTest(TestCase):
self.assertItemsEqual(urldecode('foo=bar@spam'), [('foo', 'bar@spam')])
self.assertItemsEqual(urldecode('foo=bar/baz'), [('foo', 'bar/baz')])
self.assertItemsEqual(urldecode('foo=bar?baz'), [('foo', 'bar?baz')])
+ self.assertItemsEqual(urldecode('foo=bar\'s'), [('foo', 'bar\'s')])
+ self.assertItemsEqual(urldecode('foo=$'), [('foo', '$')])
self.assertRaises(ValueError, urldecode, 'foo bar')
self.assertRaises(ValueError, urldecode, '%R')
self.assertRaises(ValueError, urldecode, '%RA')
@@ -120,11 +117,11 @@ class RequestTest(TestCase):
def test_non_unicode_params(self):
r = Request(
- bytes_type('http://a.b/path?query', 'utf-8'),
- http_method=bytes_type('GET', 'utf-8'),
- body=bytes_type('you=shall+pass', 'utf-8'),
+ b'http://a.b/path?query',
+ http_method=b'GET',
+ body=b'you=shall+pass',
headers={
- bytes_type('a', 'utf-8'): bytes_type('b', 'utf-8')
+ b'a': b'b',
}
)
self.assertEqual(r.uri, 'http://a.b/path?query')
@@ -212,6 +209,11 @@ class RequestTest(TestCase):
self.assertNotIn('bar', repr(r))
self.assertIn('<SANITIZED>', repr(r))
+ def test_headers_params(self):
+ r = Request(URI, headers={'token': 'foobar'}, body='token=banana')
+ self.assertEqual(r.headers['token'], 'foobar')
+ self.assertEqual(r.token, 'banana')
+
class CaseInsensitiveDictTest(TestCase):
diff --git a/tests/unittest/__init__.py b/tests/unittest/__init__.py
index 35b239a..6cb79a6 100644
--- a/tests/unittest/__init__.py
+++ b/tests/unittest/__init__.py
@@ -1,32 +1,15 @@
import collections
import sys
+from unittest import TestCase
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
-try:
- # check the system path first
- from unittest2 import *
-except ImportError:
- if sys.version_info >= (2, 7):
- # unittest2 features are native in Python 2.7
- from unittest import *
- else:
- raise
-
-# Python 3.1 does not provide assertIsInstance
-if sys.version_info[1] == 1:
- TestCase.assertIsInstance = lambda self, obj, cls: self.assertTrue(isinstance(obj, cls))
# Somewhat consistent itemsequal between all python versions
-if sys.version_info[1] == 3:
+if sys.version_info[0] == 3:
TestCase.assertItemsEqual = TestCase.assertCountEqual
-elif sys.version_info[0] == 2 and sys.version_info[1] == 6:
- pass
-else:
- TestCase.assertItemsEqual = lambda self, a, b: self.assertEqual(
- collections.Counter(list(a)), collections.Counter(list(b)))
# URL comparison where query param order is insignifcant
diff --git a/tox.ini b/tox.ini
index a53676f..1cac71c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,34 @@
[tox]
-envlist = py27,py34,py35,py36,pypy
+envlist = py27,py34,py35,py36,py37,pypy,pypy3,docs,readme,bandit
[testenv]
deps=
-rrequirements-test.txt
-commands=nosetests --with-coverage --cover-erase --cover-package=oauthlib -w tests
+commands=
+ py.test --cov=oauthlib tests/
-[testenv:py27]
-deps=unittest2
- {[testenv]deps}
+
+# tox -e docs to mimick readthedocs build.
+# as of today, RTD is using python2.7 and doesn't run "setup.py install"
+[testenv:docs]
+basepython=python2.7
+skipsdist=True
+deps=sphinx
+changedir=docs
+whitelist_externals=make
+commands=make clean html
+
+# tox -e readme to mimick PyPI long_description check
+[testenv:readme]
+skipsdist=True
+deps=readme
+whitelist_externals=echo
+commands=
+ python setup.py check -r -s
+ echo setup.py/long description is syntaxly correct
+
+[testenv:bandit]
+skipsdist=True
+deps=bandit
+commands=bandit -b bandit.json -r oauthlib/
+whitelist_externals=bandit