From c4626765913de97fb6410f0fdb50a4c93a38bd5b Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 6 Jan 2020 22:31:40 -0600 Subject: update authentication docs with security policy --- docs/tutorials/wiki2/authentication.rst | 136 ++++++++++++++++---------------- 1 file changed, 68 insertions(+), 68 deletions(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 3f2fcec83..580e4ba75 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -10,7 +10,7 @@ APIs to add login and logout functionality to our wiki. We will implement authentication with the following steps: -* Add an :term:`authentication policy` and a ``request.user`` computed property +* Add a :term:`security policy` and a ``request.user`` computed property (``security.py``). * Add routes for ``/login`` and ``/logout`` (``routes.py``). * Add login and logout views (``views/auth.py``). @@ -18,25 +18,24 @@ We will implement authentication with the following steps: * Add "Login" and "Logout" links to every page based on the user's authenticated state (``layout.jinja2``). * Make the existing views verify user state (``views/default.py``). -* Redirect to ``/login`` when a user is denied access to any of the views that - require permission, instead of a default "403 Forbidden" page - (``views/auth.py``). +* Redirect to ``/login`` when a user is not logged in and is denied access to any of the views that require permission (``views/auth.py``).. +* Show a custom "403 Forbidden" page if a logged in user is denied access to any views that require permission (``views/auth.py``). Authenticating requests ----------------------- -The core of :app:`Pyramid` authentication is an :term:`authentication policy` +The core of :app:`Pyramid` authentication is a :term:`security policy` which is used to identify authentication information from a ``request``, as well as handling the low-level login and logout operations required to track users across requests (via cookies, headers, or whatever else you can imagine). -Add the authentication policy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Add the security policy +~~~~~~~~~~~~~~~~~~~~~~~ -Create a new file ``tutorial/security.py`` with the following content: +Update ``tutorial/security.py`` with the following content: .. literalinclude:: src/authentication/tutorial/security.py :linenos: @@ -44,49 +43,26 @@ Create a new file ``tutorial/security.py`` with the following content: Here we've defined: -* A new authentication policy named ``MyAuthenticationPolicy``, which is - subclassed from Pyramid's - :class:`pyramid.authentication.AuthTktAuthenticationPolicy`, which tracks the - :term:`userid` using a signed cookie (lines 7-11). -* A ``get_user`` function, which can convert the ``unauthenticated_userid`` - from the policy into a ``User`` object from our database (lines 13-17). -* The ``get_user`` is registered on the request as ``request.user`` to be used - throughout our application as the authenticated ``User`` object for the - logged-in user (line 27). - -The logic in this file is a little bit interesting, so we'll go into detail -about what's happening here: - -First, the default authentication policies all provide a method named -``unauthenticated_userid`` which is responsible for the low-level parsing -of the information in the request (cookies, headers, etc.). If a ``userid`` -is found, then it is returned from this method. This is named -``unauthenticated_userid`` because, at the lowest level, it knows the value of -the userid in the cookie, but it doesn't know if it's actually a user in our -system (remember, anything the user sends to our app is untrusted). - -Second, our application should only care about ``authenticated_userid`` and -``request.user``, which have gone through our application-specific process of -validating that the user is logged in. - -In order to provide an ``authenticated_userid`` we need a verification step. -That can happen anywhere, so we've elected to do it inside of the cached -``request.user`` computed property. This is a convenience that makes -``request.user`` the source of truth in our system. It is either ``None`` or -a ``User`` object from our database. This is why the ``get_user`` function -uses the ``unauthenticated_userid`` to check the database. +* A new security policy named ``MySecurityPolicy``, which is implementing most of the :class:`pyramid.interfaces.ISecurityPolicy` interface by tracking a :term:`identity` using a signed cookie implemented by :class:`pyramid.authentication.AuthTktCookieHelper` (lines 7-29). +* The ``request.user`` computed property is registered for use throughout our application as the authenticated ``tutorial.models.User`` object for the logged-in user (line 38-39). +Our new :term:`security policy` defines how our application will remember, forget, and identify users. +It also handles authorization, which we'll cover in the next chapter (if you're wondering why we didn't implement the ``permits`` method yet). -Configure the app -~~~~~~~~~~~~~~~~~ +Identifying the current user is done in a couple steps: -Since we've added a new ``tutorial/security.py`` module, we need to include it. -Open the file ``tutorial/__init__.py`` and edit the following lines: +1. The ``MySecurityPolicy.authenticated_identity`` method asks the cookie helper to pull the identity from the request. + This value is ``None`` if the cookie is missing or the content cannot be verified. +2. We then translate the identity into a ``tutorial.models.User`` object by looking for a record in the database. + +This is a good spot to confirm that the user is actually allowed to access our application. +For example, maybe they were marked deleted or banned and we should return ``None`` instead of the ``user`` object. + +Finally, :attr:`pyramid.request.Request.authenticated_identity` contains either ``None`` or a ``tutorial.models.User`` instance and that value is aliased to ``request.user`` for convenience in our application. -.. literalinclude:: src/authentication/tutorial/__init__.py - :linenos: - :emphasize-lines: 11 - :language: python + +Configure the app +~~~~~~~~~~~~~~~~~ Our authentication policy is expecting a new setting, ``auth.secret``. Open the file ``development.ini`` and add the highlighted line below: @@ -97,7 +73,7 @@ the file ``development.ini`` and add the highlighted line below: :lineno-match: :language: ini -Finally, best practices tell us to use a different secret for production, so +Finally, best practices tell us to use a different secret in each environment, so open ``production.ini`` and add a different secret: .. literalinclude:: src/authentication/production.ini @@ -106,6 +82,14 @@ open ``production.ini`` and add a different secret: :lineno-match: :language: ini +And ``testing.ini``: + +.. literalinclude:: src/authentication/testing.ini + :lines: 17-19 + :emphasize-lines: 3 + :lineno-match: + :language: ini + Add permission checks ~~~~~~~~~~~~~~~~~~~~~ @@ -125,7 +109,7 @@ Remember our goals: Open the file ``tutorial/views/default.py`` and fix the following import: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 5-9 + :lines: 3-7 :lineno-match: :emphasize-lines: 2 :language: python @@ -135,7 +119,7 @@ Change the highlighted line. In the same file, now edit the ``edit_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 45-60 + :lines: 44-59 :lineno-match: :emphasize-lines: 5-7 :language: python @@ -148,18 +132,16 @@ If the user either is not logged in or the user is not the page's creator In the same file, now edit the ``add_page`` view function: .. literalinclude:: src/authentication/tutorial/views/default.py - :lines: 62-76 + :lines: 61- :lineno-match: :emphasize-lines: 3-5,13 :language: python Only the highlighted lines need to be changed. -If the user either is not logged in or is not in the ``basic`` or ``editor`` -roles, then we raise ``HTTPForbidden``, which will return a "403 Forbidden" -response to the user. However, we will hook this later to redirect to the login -page. Also, now that we have ``request.user``, we no longer have to hard-code -the creator as the ``editor`` user, so we can finally drop that hack. +If the user either is not logged in or is not in the ``basic`` or ``editor`` roles, then we raise ``HTTPForbidden``, which will trigger our forbidden view to compute a response. +However, we will hook this later to redirect to the login page. +Also, now that we have ``request.user``, we no longer have to hard-code the creator as the ``editor`` user, so we can finally drop that hack. These simple checks should protect our views. @@ -215,6 +197,9 @@ This code adds three new views to the application: The check is done by first finding a ``User`` record in the database, then using our ``user.check_password`` method to compare the hashed passwords. + At a privilege boundary we are sure to reset the CSRF token using :meth:`pyramid.csrf.new_csrf_token`. + If we were using sessions we would want to invalidate that as well. + If the credentials are valid, then we use our authentication policy to store the user's id in the response using :meth:`pyramid.security.remember`. @@ -227,16 +212,19 @@ This code adds three new views to the application: credentials using :meth:`pyramid.security.forget`, then redirecting them to the front page. + At a privilege boundary we are sure to reset the CSRF token using :meth:`pyramid.csrf.new_csrf_token`. + If we were using sessions we would want to invalidate that as well. + - The ``forbidden_view`` is registered using the :class:`pyramid.view.forbidden_view_config` decorator. This is a special :term:`exception view`, which is invoked when a :class:`pyramid.httpexceptions.HTTPForbidden` exception is raised. - This view will handle a forbidden error by redirecting the user to - ``/login``. As a convenience, it also sets the ``next=`` query string to the - current URL (the one that is forbidding access). This way, if the user - successfully logs in, they will be sent back to the page which they had been - trying to access. + By default, the view will return a "403 Forbidden" response and display our ``403.jinja2`` template (added below). + + However, if the user is not logged in, this view will handle a forbidden error by redirecting the user to ``/login``. + As a convenience, it also sets the ``next=`` query string to the current URL (the one that is forbidding access). + This way, if the user successfully logs in, they will be sent back to the page which they had been trying to access. Add the ``login.jinja2`` template @@ -258,9 +246,9 @@ Open ``tutorial/templates/layout.jinja2`` and add the following code as indicated by the highlighted lines. .. literalinclude:: src/authentication/tutorial/templates/layout.jinja2 - :lines: 35-46 + :lines: 35-48 :lineno-match: - :emphasize-lines: 2-10 + :emphasize-lines: 2-12 :language: html The ``request.user`` will be ``None`` if the user is not authenticated, or a @@ -269,6 +257,17 @@ make the logout link shown only when the user is logged in, and conversely the login link is only shown when the user is logged out. +Add the ``403.jinja2`` template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create ``tutorial/templates/403.jinja2`` with the following content: + +.. literalinclude:: src/authentication/tutorial/templates/403.jinja2 + :language: html + +The above template is referenced in the forbidden view that we just added in ``tutorial/views/auth.py``. + + Viewing the application in a browser ------------------------------------ @@ -287,15 +286,16 @@ following URLs, checking that the result is as expected: - http://localhost:6543/FrontPage/edit_page invokes the ``edit_page`` view for the ``FrontPage`` page object. It is executable by only the ``editor`` user. - If a different user (or the anonymous user) invokes it, then a login form - will be displayed. Supplying the credentials with the username ``editor`` and - password ``editor`` will display the edit page form. + If a different user invokes it, then the "403 Forbidden" page will be displayed. + If an anonymous user invokes it, then a login form will be displayed. + Supplying the credentials with the username ``editor`` and password ``editor`` will display the edit page form. - http://localhost:6543/add_page/SomePageName invokes the ``add_page`` view for a page. If the page already exists, then it redirects the user to the ``edit_page`` view for the page object. It is executable by either the - ``editor`` or ``basic`` user. If a different user (or the anonymous user) - invokes it, then a login form will be displayed. Supplying the credentials + ``editor`` or ``basic`` user. + If an anonymous user invokes it, then a login form will be displayed. + Supplying the credentials with either the username ``editor`` and password ``editor``, or username ``basic`` and password ``basic``, will display the edit page form. -- cgit v1.2.3 From 425c85fadfe40d016eb3320d424ec3741a47d9c4 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 7 Jan 2020 10:07:43 -0600 Subject: fix punctuation in docs/tutorials/wiki2/authentication.rst Co-Authored-By: Steve Piercy --- docs/tutorials/wiki2/authentication.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 580e4ba75..a4937d93e 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -18,7 +18,7 @@ We will implement authentication with the following steps: * Add "Login" and "Logout" links to every page based on the user's authenticated state (``layout.jinja2``). * Make the existing views verify user state (``views/default.py``). -* Redirect to ``/login`` when a user is not logged in and is denied access to any of the views that require permission (``views/auth.py``).. +* Redirect to ``/login`` when a user is not logged in and is denied access to any of the views that require permission (``views/auth.py``). * Show a custom "403 Forbidden" page if a logged in user is denied access to any views that require permission (``views/auth.py``). -- cgit v1.2.3 From 7adc44fa2b4bfa5b4230d8646e734ba262ec1ce2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 7 Jan 2020 23:48:51 -0600 Subject: demonstrate an identity_cache --- docs/tutorials/wiki2/authentication.rst | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index a4937d93e..381868e71 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -51,15 +51,30 @@ It also handles authorization, which we'll cover in the next chapter (if you're Identifying the current user is done in a couple steps: -1. The ``MySecurityPolicy.authenticated_identity`` method asks the cookie helper to pull the identity from the request. +1. :app:`Pyramid` invokes a method on the policy requesting identity, userid, or permission to perform an operation. + +1. The policy starts by calling :meth:`pyramid.request.RequestLocalCache.get_or_create` to load the identity. + +1. The ``MySecurityPolicy.load_identity`` method asks the cookie helper to pull the identity from the request. This value is ``None`` if the cookie is missing or the content cannot be verified. -2. We then translate the identity into a ``tutorial.models.User`` object by looking for a record in the database. -This is a good spot to confirm that the user is actually allowed to access our application. -For example, maybe they were marked deleted or banned and we should return ``None`` instead of the ``user`` object. +1. The policy then translates the identity into a ``tutorial.models.User`` object by looking for a record in the database. + This is a good spot to confirm that the user is actually allowed to access our application. + For example, maybe they were marked deleted or banned and we should return ``None`` instead of the ``user`` object. + +1. The result is stored in the ``identity_cache`` which ensures that subsequent invocations return the same identity object for the request. Finally, :attr:`pyramid.request.Request.authenticated_identity` contains either ``None`` or a ``tutorial.models.User`` instance and that value is aliased to ``request.user`` for convenience in our application. +Note the usage of the ``identity_cache`` is optional, but it has several advantages in most scenarios: + +- It improves performance as the identity is necessary for many operations during the lifetime of a request. + +- It provides consistency across method invocations to ensure the identity does not change while processing the request. + +It is up to individual security policies and applications to determine the best approach with respect to caching. +Applications is long-running requests may want to avoid caching the identity, or tracking some extra metadata to re-verify it periodically against the authentication source. + Configure the app ~~~~~~~~~~~~~~~~~ -- cgit v1.2.3 From 246f6ad9e6eaac613f7df2a78af3aed85bfaf79a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 10 Jan 2020 01:20:55 -0600 Subject: grammar fix --- docs/tutorials/wiki2/authentication.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 381868e71..46aa9f14a 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -73,7 +73,7 @@ Note the usage of the ``identity_cache`` is optional, but it has several advanta - It provides consistency across method invocations to ensure the identity does not change while processing the request. It is up to individual security policies and applications to determine the best approach with respect to caching. -Applications is long-running requests may want to avoid caching the identity, or tracking some extra metadata to re-verify it periodically against the authentication source. +Applications with long-running requests may want to avoid caching the identity, or tracking some extra metadata to re-verify it periodically against the authentication source. Configure the app -- cgit v1.2.3 From 3e38f884549658c41e8698a55cad0b8b3729333f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 12 Jan 2020 14:39:43 -0600 Subject: update wiki authorization chapter --- docs/tutorials/wiki2/authentication.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 46aa9f14a..c799d79bf 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -76,8 +76,8 @@ It is up to individual security policies and applications to determine the best Applications with long-running requests may want to avoid caching the identity, or tracking some extra metadata to re-verify it periodically against the authentication source. -Configure the app -~~~~~~~~~~~~~~~~~ +Add new settings +~~~~~~~~~~~~~~~~ Our authentication policy is expecting a new setting, ``auth.secret``. Open the file ``development.ini`` and add the highlighted line below: -- cgit v1.2.3 From 05f9ff6e2d1d89af68a70ab52894f6575377f78a Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 13 Jan 2020 01:49:57 -0800 Subject: Align line numbers to code - @mmerickel can you check the wording to ensure it is still correct for the code? --- docs/tutorials/wiki2/authentication.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index c799d79bf..5e0077b20 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -43,8 +43,8 @@ Update ``tutorial/security.py`` with the following content: Here we've defined: -* A new security policy named ``MySecurityPolicy``, which is implementing most of the :class:`pyramid.interfaces.ISecurityPolicy` interface by tracking a :term:`identity` using a signed cookie implemented by :class:`pyramid.authentication.AuthTktCookieHelper` (lines 7-29). -* The ``request.user`` computed property is registered for use throughout our application as the authenticated ``tutorial.models.User`` object for the logged-in user (line 38-39). +* A new security policy named ``MySecurityPolicy``, which is implementing most of the :class:`pyramid.interfaces.ISecurityPolicy` interface by tracking a :term:`identity` using a signed cookie implemented by :class:`pyramid.authentication.AuthTktCookieHelper` (lines 8-34). +* The ``request.user`` computed property is registered for use throughout our application as the authenticated ``tutorial.models.User`` object for the logged-in user (line 42-44). Our new :term:`security policy` defines how our application will remember, forget, and identify users. It also handles authorization, which we'll cover in the next chapter (if you're wondering why we didn't implement the ``permits`` method yet). -- cgit v1.2.3 From 459b0c7051f5a0c4e4ef7adf1e51e3548dba6b39 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 13 Jan 2020 01:51:37 -0800 Subject: Use reST numbered list syntax, not markdown --- docs/tutorials/wiki2/authentication.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 5e0077b20..25240b191 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -49,20 +49,20 @@ Here we've defined: Our new :term:`security policy` defines how our application will remember, forget, and identify users. It also handles authorization, which we'll cover in the next chapter (if you're wondering why we didn't implement the ``permits`` method yet). -Identifying the current user is done in a couple steps: +Identifying the current user is done in a few steps: -1. :app:`Pyramid` invokes a method on the policy requesting identity, userid, or permission to perform an operation. +#. :app:`Pyramid` invokes a method on the policy requesting identity, userid, or permission to perform an operation. -1. The policy starts by calling :meth:`pyramid.request.RequestLocalCache.get_or_create` to load the identity. +#. The policy starts by calling :meth:`pyramid.request.RequestLocalCache.get_or_create` to load the identity. -1. The ``MySecurityPolicy.load_identity`` method asks the cookie helper to pull the identity from the request. +#. The ``MySecurityPolicy.load_identity`` method asks the cookie helper to pull the identity from the request. This value is ``None`` if the cookie is missing or the content cannot be verified. -1. The policy then translates the identity into a ``tutorial.models.User`` object by looking for a record in the database. +#. The policy then translates the identity into a ``tutorial.models.User`` object by looking for a record in the database. This is a good spot to confirm that the user is actually allowed to access our application. For example, maybe they were marked deleted or banned and we should return ``None`` instead of the ``user`` object. -1. The result is stored in the ``identity_cache`` which ensures that subsequent invocations return the same identity object for the request. +#. The result is stored in the ``identity_cache`` which ensures that subsequent invocations return the same identity object for the request. Finally, :attr:`pyramid.request.Request.authenticated_identity` contains either ``None`` or a ``tutorial.models.User`` instance and that value is aliased to ``request.user`` for convenience in our application. -- cgit v1.2.3 From 6ca6370c751dd7a629f7057a79941764c71d4aeb Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 13 Jan 2020 02:07:07 -0800 Subject: Use correct verb --- docs/tutorials/wiki2/authentication.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/tutorials/wiki2/authentication.rst') diff --git a/docs/tutorials/wiki2/authentication.rst b/docs/tutorials/wiki2/authentication.rst index 25240b191..5519a967e 100644 --- a/docs/tutorials/wiki2/authentication.rst +++ b/docs/tutorials/wiki2/authentication.rst @@ -129,7 +129,7 @@ Open the file ``tutorial/views/default.py`` and fix the following import: :emphasize-lines: 2 :language: python -Change the highlighted line. +Insert the highlighted line. In the same file, now edit the ``edit_page`` view function: -- cgit v1.2.3