From d4f48845a7ceec5bbd658cf2b478f7b6d5cfee2e Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:00:45 +0100 Subject: Add OAuth2 Provider oauthlib-flow --- docs/Makefile | 2 +- docs/conf.py | 9 +- docs/oauth2/oauth2provider-legend.dot | 32 +++++ docs/oauth2/oauth2provider-server.dot | 215 ++++++++++++++++++++++++++++++++++ docs/oauth2/server.rst | 22 +++- 5 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 docs/oauth2/oauth2provider-legend.dot create mode 100644 docs/oauth2/oauth2provider-server.dot 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 2594e38..fadb913 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,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'] diff --git a/docs/oauth2/oauth2provider-legend.dot b/docs/oauth2/oauth2provider-legend.dot new file mode 100644 index 0000000..746ac2b --- /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=green]; + flow_code_auth -> b [style=bold,color=darkgreen]; + 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..bf7df75 --- /dev/null +++ b/docs/oauth2/oauth2provider-server.dot @@ -0,0 +1,215 @@ +digraph oauthlib { + 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="{Authorize Endpoint|{create_authorize_response|{uri|method|body|headers|credentials}}|{token|code}}" ]; + endpoint_token [ shape=record; label="{Token Endpoint|{create_token_response|{uri|method|body|headers|credentials}}|{authorization_code|password|client_credentials|refresh_token}}" ]; + endpoint_revoke [ shape=record; label="{Revocation Endpoint|{create_revocation_response|{uri|method|body|headers}}}" ]; + endpoint_introspect [ shape=record; label="{Introspect Endpoint|{create_introspect_response|{uri|method|body|headers}}}" ]; + endpoint_resource [ shape=record; label="{Resource Endpoint|{verify_request|{uri|method|body|headers|scopes_list}}}" ]; + } + + /* OAuthlib RequestValidator Methods */ + { + node [ shape=record ]; + + f_client_authentication_required [ label="{{client_authentication_required|request}|{True|False}}"; ]; + f_authenticate_client [ label="{{authenticate_client|request}|{True|False}}";]; + f_authenticate_client_id [ label="{{authenticate_client_id|{client_id|request}}|{True|False}}"; ]; + f_validate_grant_type [ label="{{validate_grant_type|{client_id|grant_type|client|request}}|{True|False}}"; ]; + f_validate_code [ label="{{validate_code|{client_id|code|request}}|{True|False}}"; ]; + f_confirm_redirect_uri [ label="{{confirm_redirect_uri|{client_id|code|redirect_uri|client|request}}|{True|False}}"; ]; + f_get_default_redirect_uri [ label="{{get_default_redirect_uri|{client_id|request}}|{redirect_uri|None}}"; ]; + f_invalidate_authorization_code [ label="{{invalidate_authorization_code|{client_id|code|request}}|None}"; ]; + f_validate_scopes [ label="{{validate_scopes|{client_id|scopes|client|request}}|{True|False}}"; ]; + f_save_bearer_token [ label="{{save_bearer_token|{token|request}}|None}"; ]; + f_revoke_token [ label="{{revoke_token|{token|token_type_hint|request}}|None}"; ]; + f_validate_client_id [ label="{{validate_client_id|{client_id|request}}|{True|False}}"; ]; + f_validate_redirect_uri [ label="{{validate_redirect_uri|{client_id|redirect_uri|request}}|{True|False}}"; ]; + f_is_pkce_required [ label="{{is_pkce_required|{client_id|request}}|{True|False}}"; ]; + f_validate_response_type [ label="{{validate_response_type|{client_id|response_type|client|request}}|{True|False}}"; ]; + f_save_authorization_code [ label="{{save_authorization_code|{client_id|code|request}}|None}"; ]; + f_validate_bearer_token [ label="{{validate_bearer_token|{token|scopes|request}}|{True|False}}"; ]; + f_validate_refresh_token [ label="{{validate_refresh_token|{refresh_token|client|request}}|{True|False}}"; ]; + f_get_default_scopes [ label="{{get_default_scopes|{client_id|request}}|{[scopes]}}"; ]; + f_get_original_scopes [ label="{{get_original_scopes|{refresh_token|request}}|{[scopes]}}"; ]; + f_is_within_original_scope [ label="{{is_within_original_scope|{refresh_scopes|refresh_token|request}}|{True|False}}"; ]; + f_validate_user [ label="{{validate_user|{username|password|client|request}}|{True|False}}"; ]; + f_introspect_token [ label="{{introspect_token|{token|token_type_hint|request}}|{\{claims\}|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 errors */ + e_normal [ shape=none,label="ERROR" ]; + + /* Authorization Code - Access Token Request */ + { + edge [ color=green ]; + + endpoint_token:authorization_code:s -> f_client_authentication_required; + f_client_authentication_required:true:s -> f_authenticate_client; + f_client_authentication_required:false -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_validate_grant_type; + f_authenticate_client_id:true:s -> 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_confirm_redirect_uri:true:s -> f_save_bearer_token; + f_get_default_redirect_uri -> f_save_bearer_token; + + f_save_bearer_token -> f_invalidate_authorization_code; + f_invalidate_authorization_code -> webapi_response; + } + /* Authorization Code - Authorization Request */ + { + edge [ color=darkgreen ]; + + 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 -> 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 -> f_validate_scopes; + + if_code_challenge -> f_validate_scopes [ label="present" ]; + if_code_challenge -> e_normal [ label="missing" ]; + + f_validate_scopes:true:s -> f_save_authorization_code; + } + + /* 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 -> f_validate_response_type; + f_validate_response_type:true:s -> f_validate_scopes; + f_validate_scopes:true:s -> f_save_bearer_token; + } + + /* 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 -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_validate_user; + f_authenticate_client_id:true:s -> 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 -> 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 -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_validate_grant_type; + f_authenticate_client_id:true:s -> 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 [ label="" ]; + f_client_authentication_required:true:s -> f_authenticate_client; + f_client_authentication_required:false -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_introspect_token; + f_authenticate_client_id:true:s -> 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 -> f_authenticate_client_id; + f_authenticate_client:true:s -> f_revoke_token; + f_authenticate_client_id:true:s -> 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/server.rst b/docs/oauth2/server.rst index 6c065c5..dad0aae 100644 --- a/docs/oauth2/server.rst +++ b/docs/oauth2/server.rst @@ -25,7 +25,17 @@ as well as provide an interface for a backend to store tokens, clients, etc. .. 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 @@ -257,7 +267,7 @@ the token. challenge_method = django.db.models.CharField(max_length=6) -2. Implement a validator +3. Implement a validator ------------------------ The majority of the work involved in implementing an OAuth 2 provider @@ -301,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 @@ -326,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 @@ -430,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. @@ -501,7 +511,7 @@ 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 `Gitter OAuthLib community`_ or open a `GitHub issue`_ =) -- cgit v1.2.1 From 45135a25d4dde6f0d1d6a9b735a40159ac391c11 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:23:57 +0100 Subject: Update Changelog to 3.0.0 --- CHANGELOG.rst | 42 ++++++++++++++++++++++++++++++++++++++++-- oauthlib/__init__.py | 2 +- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fd53769..3dea103 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,33 @@ Changelog ========= -Unreleased +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 - Bugfixes + +* Add "request" to confirm_redirect_uri #504 +* confirm_redirect_uri/get_default_redirect_uri has a bit changed #445 +* empty scopes no longer raise exceptions for implicit and authorization_code #475 / #406 +* 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 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. @@ -11,6 +35,20 @@ Unreleased 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) ------------------ diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index 5b1b380..e4ee2fd 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -12,6 +12,6 @@ import logging from logging import NullHandler __author__ = 'The OAuthlib Community' -__version__ = '3.0.0-dev' +__version__ = '3.0.0' logging.getLogger('oauthlib').addHandler(NullHandler()) -- cgit v1.2.1 From f4273e75250dd36c88d63dc075ae45650a5172e9 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:24:11 +0100 Subject: Bump to 2019 --- LICENSE | 2 +- docs/conf.py | 2 +- oauthlib/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 84b5c75..d5a9e9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018 The OAuthlib Community +Copyright (c) 2019 The OAuthlib Community All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/conf.py b/docs/conf.py index fadb913..bd8750e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,7 +46,7 @@ master_doc = 'index' # General information about the project. project = u'OAuthLib' -copyright = u'2018, The OAuthlib 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 diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py index e4ee2fd..b37d288 100644 --- a/oauthlib/__init__.py +++ b/oauthlib/__init__.py @@ -5,7 +5,7 @@ A generic, spec-compliant, thorough implementation of the OAuth request-signing logic. - :copyright: (c) 2018 by The OAuthlib Community + :copyright: (c) 2019 by The OAuthlib Community :license: BSD, see LICENSE for details. """ import logging -- cgit v1.2.1 From 213a47cf5fc9672271d98b898683727dafe0570b Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Fri, 28 Dec 2018 19:24:30 +0100 Subject: Replace latest occurences of Gazit w/ new community --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index bd8750e..f1a489a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -191,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 @@ -221,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. @@ -235,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'), ] -- cgit v1.2.1 From fa0b1549546d8c7dc1045ea637a8f8afd0d39a83 Mon Sep 17 00:00:00 2001 From: Jonathan Huot Date: Mon, 7 Jan 2019 10:22:22 +0100 Subject: Add Breaking Changes section & split Bugfixes --- CHANGELOG.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3dea103..2cc0dd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,17 +10,20 @@ OAuth2.0 Provider - outstanding Features * RFC8414 OAuth2.0 Authorization Server Metadata support (#605) * RFC7636 PKCE support (#617 #624) -OAuth2.0 Provider - Bugfixes +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 -* empty scopes no longer raise exceptions for implicit and authorization_code #475 / #406 * 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 + - 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: -- cgit v1.2.1