20: Logins With Authentication

Login views that authenticate a username/password against a list of users.

Background

Most web applications have URLs that allow people to add/edit/delete content via a web browser. Time to add security to the application. In this first step we introduce authentication. That is, logging in and logging out using Pyramid's rich facilities for pluggable user storages.

In the next step we will introduce protection resources with authorization security statements.

Objectives

  • Introduce the Pyramid concepts of authentication
  • Create login/logout views

Steps

  1. We are going to use the view classes step as our starting point:

    $ cd ..; cp -r view_classes authentication; cd authentication
    $ $VENV/bin/python setup.py develop
    
  2. Put the security hash in the authentication/development.ini configuration file as tutorial.secret instead of putting it in the code:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    [app:main]
    use = egg:tutorial
    pyramid.reload_templates = true
    pyramid.includes =
        pyramid_debugtoolbar
    tutorial.secret = 98zd
    
    [server:main]
    use = egg:pyramid#wsgiref
    host = 0.0.0.0
    port = 6543
    
  3. Get authentication (and for now, authorization policies) and login route into the configurator in authentication/tutorial/__init__.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    from pyramid.authentication import AuthTktAuthenticationPolicy
    from pyramid.authorization import ACLAuthorizationPolicy
    from pyramid.config import Configurator
    
    from .security import groupfinder
    
    
    def main(global_config, **settings):
        config = Configurator(settings=settings)
        config.include('pyramid_chameleon')
    
        # Security policies
        authn_policy = AuthTktAuthenticationPolicy(
            settings['tutorial.secret'], callback=groupfinder,
            hashalg='sha512')
        authz_policy = ACLAuthorizationPolicy()
        config.set_authentication_policy(authn_policy)
        config.set_authorization_policy(authz_policy)
    
        config.add_route('home', '/')
        config.add_route('hello', '/howdy')
        config.add_route('login', '/login')
        config.add_route('logout', '/logout')
        config.scan('.views')
        return config.make_wsgi_app()
    
  4. Create a authentication/tutorial/security.py module that can find our user information by providing an authentication policy callback:

    1
    2
    3
    4
    5
    6
    7
    8
    USERS = {'editor': 'editor',
             'viewer': 'viewer'}
    GROUPS = {'editor': ['group:editors']}
    
    
    def groupfinder(userid, request):
        if userid in USERS:
            return GROUPS.get(userid, [])
    
  5. Update the views in authentication/tutorial/views.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    from pyramid.httpexceptions import HTTPFound
    from pyramid.security import (
        remember,
        forget,
        )
    
    from pyramid.view import (
        view_config,
        view_defaults
        )
    
    from .security import USERS
    
    
    @view_defaults(renderer='home.pt')
    class TutorialViews:
        def __init__(self, request):
            self.request = request
            self.logged_in = request.authenticated_userid
    
        @view_config(route_name='home')
        def home(self):
            return {'name': 'Home View'}
    
        @view_config(route_name='hello')
        def hello(self):
            return {'name': 'Hello View'}
    
        @view_config(route_name='login', renderer='login.pt')
        def login(self):
            request = self.request
            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 = ''
            login = ''
            password = ''
            if 'form.submitted' in request.params:
                login = request.params['login']
                password = request.params['password']
                if USERS.get(login) == password:
                    headers = remember(request, login)
                    return HTTPFound(location=came_from,
                                     headers=headers)
                message = 'Failed login'
    
            return dict(
                name='Login',
                message=message,
                url=request.application_url + '/login',
                came_from=came_from,
                login=login,
                password=password,
            )
    
        @view_config(route_name='logout')
        def logout(self):
            request = self.request
            headers = forget(request)
            url = request.route_url('home')
            return HTTPFound(location=url,
                             headers=headers)
    
  6. Add a login template at authentication/tutorial/login.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Quick Tutorial: ${name}</title>
    </head>
    <body>
    <h1>Login</h1>
    <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"
               value="${login}"/><br/>
        <label for="password">Password</label>
        <input type="password" id="password"
               name="password"
               value="${password}"/><br/>
        <input type="submit" name="form.submitted"
               value="Log In"/>
    </form>
    </body>
    </html>
    
  7. Provide a login/logout box in authentication/tutorial/home.pt

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Quick Tutorial: ${name}</title>
    </head>
    <body>
    
    <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>
    </div>
    
    <h1>Hi ${name}</h1>
    <p>Visit <a href="${request.route_url('hello')}">hello</a></p>
    </body>
    </html>
    
  8. Run your Pyramid application with:

    $ $VENV/bin/pserve development.ini --reload
    
  9. Open http://localhost:6543/ in a browser.

  10. Click the "Log In" link.

  11. Submit the login form with the username editor and the password editor.

  12. Note that the "Log In" link has changed to "Logout".

  13. Click the "Logout" link.

Analysis

Unlike many web frameworks, Pyramid includes a built-in but optional security model for authentication and authorization. This security system is intended to be flexible and support many needs. In this security model, authentication (who are you) and authorization (what are you allowed to do) are not just pluggable, but de-coupled. To learn one step at a time, we provide a system that identifies users and lets them log out.

In this example we chose to use the bundled AuthTktAuthenticationPolicy policy. We enabled it in our configuration and provided a ticket-signing secret in our INI file.

Our view class grew a login view. When you reached it via a GET, it returned a login form. When reached via POST, it processed the username and password against the "groupfinder" callable that we registered in the configuration.

In our template, we fetched the logged_in value from the view class. We use this to calculate the logged-in user, if any. In the template we can then choose to show a login link to anonymous visitors or a logout link to logged-in users.

Extra Credit

  1. What is the difference between a user and a principal?
  2. Can I use a database behind my groupfinder to look up principals?
  3. Once I am logged in, does any user-centric information get jammed onto each request? Use import pdb; pdb.set_trace() to answer this.