pvl/login/server.py
author Tero Marttila <terom@paivola.fi>
Mon, 13 Jan 2014 02:46:18 +0200
changeset 350 1ca04394c314
parent 349 3c20473d0bdc
child 351 147f5e86b139
permissions -rw-r--r--
pvl.login.server: logout
# encoding: utf-8

import datetime
import werkzeug
import werkzeug.urls

import pvl.web
import pvl.web.response

from pvl.login import pubtkt
from pvl.web import urls, html

import logging; log = logging.getLogger('pvl.login.server')

class Handler (pvl.web.Handler) :
    # Bootstrap
    DOCTYPE = 'html'
    HTML_XMLNS = None
    HTML_LANG = 'en'
    CSS = (
            '//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css',
    )
    JS = (
            '//code.jquery.com/jquery.js',
            '//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js',
    )

    def redirect (self, *url, **params) :
        return pvl.web.response.redirect(self.url(*url, **params))

    def process_cookie (self) :
        """
            Reverse the urlencoding used for the cookie...
        """
        
        log.debug("cookies: %s", self.request.cookies)

        cookie = self.request.cookies.get(self.app.cookie_name)
        
        log.debug("cookie %s=%s", self.app.cookie_name, cookie)

        if cookie :
            cookie = werkzeug.urls.url_unquote(cookie)
        
        log.debug("cookie decoded: %s", cookie)
        
        if cookie :
            return self.app.load(cookie)

class Index (Handler) :
    TITLE = u"Päivölä Network Login"
    
    pubtkt = None
    cookie_error = None

    def process (self) :
        try :
            self.pubtkt = self.process_cookie()
        except pubtkt.Error as ex :
            self.cookie_error = ex
            
        if not self.pubtkt :
            return self.redirect(Login)

    def render_valid (self, valid) :
        seconds = valid.seconds + valid.days * (24 * 60 * 60)
        
        minutes, seconds = divmod(seconds, 60)
        hours, minutes = divmod(minutes, 60)

        return "%2d:%02d:%02d" % (hours, minutes, seconds)

    def render_pubtkt_fields (self, pubtkt) :
        """
            Yield (glyphicon, text) to render as fields for ticket.
        """

        yield 'user', "User account", pubtkt.uid
        
        valid = self.render_valid(pubtkt.valid())
        
        if pubtkt.graceperiod :
            valid = "{valid} ({grace})".format(valid=valid, grace=self.render_valid(pubtkt.grace()))

        yield 'time', "Remaining validity", valid

        if pubtkt.cip :
            yield 'cloud', "Network address", pubtkt.cip

        if pubtkt.udata :
            yield 'comment', "Associated data", pubtkt.udata

        for token in pubtkt.tokens :
            yield 'flag', "Access token", token

        if pubtkt.bauth :
            yield 'keys', "Authentication token", pubtkt.bauth

    def render_pubtkt (self, pubtkt) :
        return html.div(class_='panel panel-info')(
            html.div(class_='panel-heading')("Login: {pubtkt.uid}".format(pubtkt=self.pubtkt)),
            html.div(class_='panel-body')(
                "This is a valid login ticket.",
            ),
            html.ul(class_='list-group')(
                html.li(class_='list-group-item', title=title)(
                    html.span(class_='glyphicon glyphicon-{glyphicon}'.format(glyphicon=icon)) if icon else None,
                    info,
                ) for icon, title, info in self.render_pubtkt_fields(pubtkt)
            ),
            html.div(class_='panel-footer')(
                html.form(action='/logout', method='post')(
                    html.button(type='submit', class_='btn btn-warning')("Logout"),
                ),
            ),
        )

    def render_info (self) :
        if self.cookie_error :
            return (
                    html.h2("Invalid cookie"),
                    html.p(self.cookie_error),
            )

        return self.render_pubtkt(self.pubtkt)
   
    def render (self) :

        return html.div(class_='container')(
                self.render_info(),
        )

class Login (Handler) :
    TITLE = "Login"
    
    STYLE = """
form#login {
    max-width:  50%;
    padding:    1em;
    margin:     0 auto;
}

    """


    auth_error = None

    def process (self) :
        if self.request.method == 'POST' :
            back = self.app.login_server
            username = self.request.form.get('username')
            password = self.request.form.get('username')

            if username and password :
                # preprocess
                username = username.strip().lower()
                
                try :
                    pt = self.app.auth(username, password)

                except pubtkt.Error as ex :
                    self.auth_errors = ex

                else :
                    # browsers seem to be very particular about quoting ;'s in cookie values...
                    # this follows PHP's setcookie() encoding...
                    cookie = werkzeug.urls.url_quote(self.app.sign(pt))

                    # redirect with cookie
                    response = pvl.web.response.redirect(back)

                    response.set_cookie(self.app.cookie_name, cookie,
                        domain      = self.app.cookie_domain,
                        secure      = self.app.cookie_secure,
                        httponly    = self.app.cookie_httponly,
                    )

                    return response

    def render (self) :
        domain = self.app.login_domain

        return html.div(class_='container')(
            html.form(action=self.url(), method='POST', id='login')(
                html.fieldset(
                    html.legend("Log in"),
                
                    html.div(class_='form-group')(
                        html.div(class_='input-group')(
                            html.label(for_='username', class_='sr-only')("Username"),
                            html.input(name='username', type='text', class_='form-control', placeholder="username", required=True, autofocus=True),
                            html.span(class_='input-group-addon')("@{domain}".format(domain=domain)),
                        ),

                        html.label(for_='password', class_='sr-only')("Password"),
                        html.input(name='password', type='password', class_='form-control', placeholder="Password", required=True),
                    ),

                    html.button(type='submit', class_='btn btn-primary')("Login"),
                )
            )
        )

class Logout (Handler) :
    TITLE = "Logout"

    def process (self) :
        try :
            self.pubtkt = self.process_cookie() 
        except Error as ex :
            self.pubtkt_error = ex
            self.pubtkt = ex.pubtkt

        if not self.pubtkt :
            return self.redirect(Index)

        if self.request.method == 'POST' :
            back = self.app.login_server

            response = pvl.web.response.redirect(back)

            response.set_cookie(self.app.cookie_name, '',
                    expires = 0,
                    domain      = self.app.cookie_domain,
                    secure      = self.app.cookie_secure,
                    httponly    = self.app.cookie_httponly,
            )
            
            return response
    
    def render (self) :
        return html.div(class_='container')(
            html.form(action=self.url(), method='post')(
                html.fieldset(
                    html.legend("Logout {pubtkt.uid}".format(pubtkt=self.pubtkt)),
            
                    html.button(type='submit', class_='btn btn-warning')("Logout"),
                )
            )
        )

class LoginApplication (pvl.web.Application) :
    URLS = urls.Map((
        urls.rule('/',              Index),
        urls.rule('/login',         Login),
        urls.rule('/logout',        Logout),
    ))

    PUBLIC_KEY = 'etc/login/public.pem'
    PRIVATE_KEY = 'etc/login/private.pem'
    
    login_domain = 'test.paivola.fi'
    login_server = 'https://login.test.paivola.fi/'
    login_expire = datetime.timedelta(hours=1)

    cookie_name = 'auth_pubtkt'
    cookie_domain = 'test.paivola.fi'
    cookie_secure = True
    cookie_httponly = True

    def __init__ (self, public_key=PUBLIC_KEY, private_key=PRIVATE_KEY, **opts) :
        super(LoginApplication, self).__init__(**opts)
        
        self.server_keys = pubtkt.ServerKeys.config(
                public_key  = public_key,
                private_key = private_key,
        )

    def load (self, cookie) :
        """
            Load a pubtkt from a cookie, and verify it.
        """

        return pubtkt.PubTkt.load(cookie, self.server_keys.public)

    def auth (self, username, password) :
        """
            Perform authentication, returning a PubTkt, signed
        """
        
        return pubtkt.PubTkt.new(username,
                expiry  = self.login_expire,
        )

    def sign (self, pubtkt) :
        """
            Create a cookie by signing the given pubtkt.
        """
        
        return pubtkt.sign(self.server_keys.private)