summaryrefslogtreecommitdiff
path: root/docs/tutorials/wiki2/authentication.rst
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2020-01-06 22:31:40 -0600
committerMichael Merickel <michael@merickel.org>2020-01-06 22:58:26 -0600
commitc4626765913de97fb6410f0fdb50a4c93a38bd5b (patch)
tree7d61935e7cc115ed3a9b9b2ee47df009efeeceae /docs/tutorials/wiki2/authentication.rst
parentcd666082fbbd8b11d5cefd4a2d72209ae4f847be (diff)
downloadpyramid-c4626765913de97fb6410f0fdb50a4c93a38bd5b.tar.gz
pyramid-c4626765913de97fb6410f0fdb50a4c93a38bd5b.tar.bz2
pyramid-c4626765913de97fb6410f0fdb50a4c93a38bd5b.zip
update authentication docs with security policy
Diffstat (limited to 'docs/tutorials/wiki2/authentication.rst')
-rw-r--r--docs/tutorials/wiki2/authentication.rst136
1 files changed, 68 insertions, 68 deletions
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.