Wiki Flow of Authentication¶
警告
This recipe has not received significant updates since its creation around the time Pyramid 1.0 was released. Since then, the wiki tutorial to which this recipe refers has received numerous significant updates. Pyramid 1.6.1 was released on 2016-02-02, and a major update to the wiki tutorial has been merged for the Pyramid 1.7 release. Upon the release of Pyramid 1.7, this recipe will be removed as obsolete.
This tutorial describes the "flow of authentication" of the result of the completing the Adding authorization tutorial chapter from the main Pyramid documentation.
This text was contributed by John Shipman.
Overall flow of an authentication¶
Now that you have seen all the pieces of the authentication mechanism, here are some examples that show how they all work together.
Failed login: The user requests
/FrontPage/edit_page
. The site presents the login form. The user enterseditor
as the login, but enters an invalid passwordbad
. The site redisplays the login form with the message "Failed login". See Failed login.The user again requests
/FrontPage/edit_page
. The site presents the login form, and this time the user enters logineditor
and passwordeditor
. The site presents the edit form with the content of/FrontPage
. The user makes some changes and saves them. See Successful login.The user again revisits
/FrontPage/edit_page
. The site goes immediately to the edit form without requesting credentials. See Revisiting after authentication.The user clicks the
Logout
link. See Logging out.
Failed login¶
The process starts when the user enters URL
http://localhost:6543/FrontPage/edit_page
. Let's assume that
this is the first request ever made to the application and the
page database is empty except for the Page
instance created
for the front page by the initialize_sql
function in
models.py
.
This process involves two complete request/response cycles.
From the front page, the user clicks Edit page. The request is to
/FrontPage/edit_page
. The view callable islogin.login
. The response is thelogin.pt
template with blank fields.The user enters invalid credentials and clicks Log in. A
POST
request is sent to/FrontPage/edit_page
. The view callable is againlogin.login
. The response is thelogin.pt
template showing the message "Failed login", with the entry fields displaying their former values.
Cycle 1:
During URL dispatch, the route
'/{pagename}/edit_page'
is considered for matching. The associated view has aview_permission='edit'
permission attached, so the dispatch logic has to verify that the user has that permission or the route is not considered to match.The context for all route matching comes from the configured root factory,
RootFactory()
inmodels.py
. This class has an__acl__
attribute that defines the access control list for all routes:__acl__ = [ (Allow, Everyone, 'view'), (Allow, 'group:editors', 'edit') ]
In practice, this means that for any route that requires the
edit
permission, the user must be authenticated and have thegroup:editors
principal or the route is not considered to match.To find the list of the user's principals, the authorization first policy checks to see if the user has a
paste.auth.auth_tkt
cookie. Since the user has never been to the site, there is no such cookie, and the user is considered to be unauthenticated.Since the user is unauthenticated, the
groupfinder
function insecurity.py
is called withNone
as itsuserid
argument. The function returns an empty list of principals.Because that list does not contain the
group:editors
principal, the'/{pagename}/edit_page'
route'sedit
permission fails, and the route does not match.Because no routes match, the forbidden view callable is invoked: the
login
function in modulelogin.py
.Inside the
login
function, the value oflogin_url
ishttp://localhost:6543/login
, and the value ofreferrer
ishttp://localhost:6543/FrontPage/edit_page
.Because
request.params
has no key for'came_from'
, the variablecame_from
is also set tohttp://localhost:6543/FrontPage/edit_page
. Variablesmessage
,login
, andpassword
are set to the empty string.Because
request.params
has no key for'form.submitted'
, thelogin
function returns this dictionary:{'message': '', 'url':'http://localhost:6543/login', 'came_from':'http://localhost:6543/FrontPage/edit_page', 'login':'', 'password':''}
This dictionary is used to render the
login.pt
template. In the form, theaction
attribute ishttp://localhost:6543/login
, and the value ofcame_from
is included in that form as a hidden field by this line in the template:<input type="hidden" name="came_from" value="${came_from}"/>
Cycle 2:
The user enters incorrect credentials and clicks the Log in button, which does a
POST
request to URLhttp://localhost:6543/login
. The name of the Log in button in this form isform.submitted
.The route with pattern
'/login'
matches this URL, so control is passed again to thelogin
view callable.The
login_url
andreferrer
have the same value this time (http://localhost:6543/login
), so variablereferrer
is set to'/'
.Since
request.params
does have a key'form.submitted'
, the values oflogin
andpassword
are retrieved fromrequest.params
.Because the login and password do not match any of the entries in the
USERS
dictionary insecurity.py
, variablemessage
is set to'Failed login'
.The view callable returns this dictionary:
{'message':'Failed login', 'url':'http://localhost:6543/login', 'came_from':'/', 'login':'editor', 'password':'bad'}
The
login.pt
template is rendered using those values.
Successful login¶
In this scenario, the user again requests URL
/FrontPage/edit_page
.
This process involves four complete request/response cycles.
The user clicks Edit page. The view callable is
login.login
. The response is templatelogin.pt
, with all the fields blank.The user enters valid credentials and clicks Log in. The view callable is
login.login
. The response is a redirect to/FrontPage/edit_page
.The view callable is
views.edit_page
. The response renders templateedit.pt
, displaying the current page content.The user edits the content and clicks Save. The view callable is
views.edit_page
. The response is a redirect to/FrontPage
.
Execution proceeds as in Failed login, up to the point
where the password editor
is successfully matched against the
value from the USERS
dictionary.
Cycle 2:
Within the
login.login
view callable, the value oflogin_url
ishttp://localhost:6543/login
, and the value ofreferrer
is'/'
, andcame_from
ishttp://localhost:6543/FrontPage/edit_page
when this block is executed:if USERS.get(login) == password: headers = remember(request, login) return HTTPFound(location=came_from, headers=headers)
Because the password matches this time,
pyramid.security.remember
returns a sequence of header tuples that will set apaste.auth.auth_tkt
authentication cookie in the user's browser for the login'editor'
.The
HTTPFound
exception returns a response that redirects the browser tohttp://localhost:6543/FrontPage/edit_page
, including the headers that set the authentication cookie.
Cycle 3:
Route pattern
'/{pagename}/edit_page'
matches this URL, but the corresponding view is restricted by an'edit'
permission.Because the user now has an authentication cookie defining their login name as
'editor'
, thegroupfinder
function is called with that value as itsuserid
argument.The
groupfinder
function returns the list['group:editors']
. This satisfies the access control entry(Allow, 'group:editors', 'edit')
, which grants theedit
permission. Thus, this route matches, and control passes to view callableedit_page
.Within
edit_page
,name
is set to'FrontPage'
, the page name fromrequest.matchdict['pagename']
, andpage
is set to an instance ofmodels.Page
that holds the current content ofFrontPage
.Since this request did not come from a form,
request.params
does not have a key for'form.submitted'
.The
edit_page
function callspyramid.security.authenticated_userid()
to find out whether the user is authenticated. Because of the cookies set previously, the variablelogged_in
is set to the userid'editor'
.The
edit_page
function returns this dictionary:{'page':page, 'logged_in':'editor', 'save_url':'http://localhost:6543/FrontPage/edit_page'}
Template
edit.pt
is rendered with those values. Among other features of this template, these lines cause the inclusion of a Logout link:<span tal:condition="logged_in"> <a href="${request.application_url}/logout">Logout</a> </span>
For the example case, this link will refer to
http://localhost:6543/logout
.These lines of the template display the current page's content in a form whose
action
attribute ishttp://localhost:6543/FrontPage/edit_page
:<form action="${save_url}" method="post"> <textarea name="body" tal:content="page.data" rows="10" cols="60"/> <input type="submit" name="form.submitted" value="Save"/> </form>
Cycle 4:
The user edits the page content and clicks Save.
URL
http://localhost:6543/FrontPage/edit_page
goes through the same routing as before, up until the line that checks whetherrequest.params
has a key'form.submitted'
. This time, within theedit_page
view callable, these lines are executed:page.data = request.params['body'] session.add(page) return HTTPFound(location = route_url('view_page', request, pagename=name))
The first two lines replace the old page content with the contents of the
body
text area from the form, and then update the page stored in the database. The third line causes a response that redirects the browser tohttp://localhost:6543/FrontPage
.
Revisiting after authentication¶
In this case, the user has an authentication cookie set in their
browser that specifies their login as 'editor'
. The
requested URL is http://localhost:6543/FrontPage/edit_page
.
This process requires two request/response cycles.
The user clicks Edit page. The view callable is
views.edit_page
. The response isedit.pt
, showing the current page content.The user edits the content and clicks Save. The view callable is
views.edit_page
. The response is a redirect to/Frontpage
.
Cycle 1:
The route with pattern
/{pagename}/edit_page
matches the URL, and because of the authentication cookie,groupfinder
returns a list containing thegroup:editors
principal, whichmodels.RootFactory.__acl__
uses to grant theedit
permission, so this route matches and dispatches to the view callableviews.edit_page()
.In
edit_page
, because the request did not come from a form submission,request.params
has no key for'form.submitted'
.The variable
logged_in
is set to the login name'editor'
by callingauthenticated_userid
, which extracts it from the authentication cookie.The function returns this dictionary:
{'page':page, 'save_url':'http://localhost:6543/FrontPage/edit_page', 'logged_in':'editor'}
Template
edit.pt
is rendered with the values from that dictionary. Because of the presence of the'logged_in'
entry, a Logout link appears.
Cycle 2:
The user edits the page content and clicks Save.
The
POST
operation works as in Successful login.
Logging out¶
This process starts with a request URL
http://localhost:6543/logout
.
The route with pattern
'/logout'
matches and dispatches to the view callablelogout
inlogin.py
.The call to
pyramid.security.forget()
returns a list of header tuples that will, when returned with the response, cause the browser to delete the user's authentication cookie.The view callable returns an
HTTPFound
exception that redirects the browser to named routeview_wiki
, which will translate to URLhttp://localhost:6543
. It also passes along the headers that delete the authentication cookie.