Skip to content

Commit

Permalink
fix: store 'came_from' information in the session
Browse files Browse the repository at this point in the history
- As with the previous commit, we want to avoid trusting user-supplied data
  from the query string or form parameters when constructing redirect URLs.

- Storing the route name and matchdict for the view being forbidden in
  the session allows us to construct the redirect URL on successful
  login cleanly.

- In order to clarify that the logic of storing the 'came from'
  information is separate from rendering or processing the login form,
  this PR splits the `@forbidden_view` mapping onto a separate view function.
  • Loading branch information
tseaver committed Jun 10, 2024
1 parent c923514 commit e72d437
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 18 deletions.
14 changes: 11 additions & 3 deletions docs/quick_tutorial/authorization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,17 @@ Of course, this only applies on ``Root``. Some other part of the site (a.k.a.
*context*) might have a different ACL.

If you are not logged in and visit ``/howdy``, you need to get shown the login
screen. How does Pyramid know what is the login page to use? We explicitly told
Pyramid that the ``login`` view should be used by decorating the view with
``@forbidden_view_config``.
screen. How does Pyramid know what is the login page to use? We defined an
explicit "forbidden view", decorating that view with
``@forbidden_view_config``, and then had it store the information about the
route being protected in the request's session, before redirecting to the
login view.

.. note::

We use the session to store the ``came_from`` information, rather than a
hidden form input, in order to avoid trusting user-supplied data (from the
form or query string) when constructing redirect URLs.


Extra credit
Expand Down
9 changes: 7 additions & 2 deletions docs/quick_tutorial/authorization/tutorial/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from pyramid.config import Configurator
from pyramid.session import SignedCookieSessionFactory

from .security import SecurityPolicy


def main(global_config, **settings):
config = Configurator(settings=settings,
root_factory='.resources.Root')
my_session_factory = SignedCookieSessionFactory('itsaseekreet')
config = Configurator(
settings=settings,
root_factory='.resources.Root',
session_factory=my_session_factory,
)
config.include('pyramid_chameleon')

config.set_security_policy(
Expand Down
6 changes: 4 additions & 2 deletions docs/quick_tutorial/authorization/tutorial/home.pt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
<div>
<a tal:condition="view.logged_in is None"
href="${request.application_url}/login">Log In</a>
<a tal:condition="view.logged_in is not None"
href="${request.application_url}/logout">Logout</a>
<span tal:condition="view.logged_in is not None">
<a href="${request.application_url}/logout">Logout</a>
as ${view.logged_in}
</span>
</div>

<h1>Hi ${name}</h1>
Expand Down
2 changes: 0 additions & 2 deletions docs/quick_tutorial/authorization/tutorial/login.pt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
<span tal:replace="message"/>

<form action="${url}" method="post">
<input type="hidden" name="came_from"
value="${came_from}"/>
<label for="login">Username</label>
<input type="text" id="login"
name="login"
Expand Down
43 changes: 34 additions & 9 deletions docs/quick_tutorial/authorization/tutorial/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,58 @@ def home(self):
def hello(self):
return {'name': 'Hello View'}

@forbidden_view_config()
def forbidden(self):
request = self.request
session = request.session
if request.matched_route is not None:
session['came_from'] = {
'route_name': request.matched_route.name,
'route_kwargs': request.matchdict,
}
if request.authenticated_userid is not None:
session['message'] = (
f'User {request.authenticated_userid} is not allowed '
f'to see this resource. Please log in as another user.'
)
else:
if 'came_from' in session:
del session['came_from']

return HTTPFound(request.route_url('login'))

@view_config(route_name='login', renderer='login.pt')
@forbidden_view_config(renderer='login.pt')
def login(self):
request = self.request
session = request.session
login_url = request.route_url('login')
referrer = request.url
if referrer == login_url:
referrer = '/' # never use login form itself as came_from
came_from = request.params.get('came_from', referrer)
message = ''
came_from = session.get('came_from')
message = session.get('message', '')
login = ''
password = ''

if 'form.submitted' in request.params:
login = request.params['login']
password = request.params['password']
hashed_pw = USERS.get(login)
if hashed_pw and check_password(password, hashed_pw):
headers = remember(request, login)
return HTTPFound(location=came_from,
headers=headers)

if came_from is not None:
return_to = request.route_url(
came_from['route_name'], **came_from['route_kwargs'],
)
else:
return_to = request.route_url('home')

return HTTPFound(location=return_to, headers=headers)

message = 'Failed login'

return dict(
name='Login',
message=message,
url=request.application_url + '/login',
came_from=came_from,
login=login,
password=password,
)
Expand Down

0 comments on commit e72d437

Please sign in to comment.