pvl/login/server.py
author Tero Marttila <terom@paivola.fi>
Mon, 13 Jan 2014 03:20:04 +0200
changeset 351 147f5e86b139
parent 350 1ca04394c314
child 354 d46c8d3e3140
permissions -rw-r--r--
pvl.login: fix validity logic, implement renew
# 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))

    pubtkt = None
    cookie_error = None
    verify_error = None

    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)
        
        if not cookie :
            return

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

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

        try :
            self.pubtkt = self.app.load(cookie)

        except pubtkt.ParseError as ex :
            self.cookie_error = ex

        except pubtkt.VerifyError as ex :
            self.pubtkt = ex.pubtkt
            self.verify_error = ex
 

class Index (Handler) :
    TITLE = u"Päivölä Network Login"
    
    def process (self) :
        self.process_cookie()
            
        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_status (self, pubtkt) :
        valid = pubtkt.valid()
        grace = pubtkt.grace()

        if valid and grace :
            return 'success'
        elif valid and grace is False :
            return 'warning'
        elif valid :
            return 'success'
        else :
            return 'danger'

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

        yield 'user', None, "User account", pubtkt.uid
        
        valid = pubtkt.valid()
        grace = pubtkt.grace()

        if valid and grace :
            valid = "{valid} ({grace})".format(valid=self.render_valid(valid), grace=self.render_valid(grace))
            valid_status = 'success'
        elif valid and grace is False :
            valid = "Renewable ({grace})".format(valid=self.render_valid(valid), grace=self.render_valid(grace))
            valid_status = 'warning'
        elif valid :
            valid = "{valid}".format(valid=self.render_valid(valid))
            valid_status = 'success'
        else :
            valid = "Expired"
            valid_status = 'danger'

        yield 'time', valid_status, "Remaining validity", valid

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

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

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

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

    def render_pubtkt (self, pubtkt) :
        status = self.render_status(pubtkt)

        return html.div(class_='panel panel-{status}'.format(status=status))(
            html.div(class_='panel-heading')("Login: {pubtkt.uid}".format(pubtkt=self.pubtkt)),
            html.ul(class_='list-group')(
                html.li(class_='list-group-item {status}'.format(status=('alert-'+status if status else '')), title=title)(
                    html.span(class_='glyphicon glyphicon-{glyphicon}'.format(glyphicon=icon)) if icon else None,
                    info,
                ) for icon, status, title, info in self.render_pubtkt_fields(pubtkt)
            ),
            html.div(class_='panel-footer')(
                (
                    html.form(action=self.url(Login), method='post')(
                        html.button(type='submit', class_='btn btn-success')("Renew"),
                    )
                ) if pubtkt.valid() else (
                    html.form(action=self.url(Login), method='get')(
                        html.button(type='submit', class_='btn btn-info')("Login"),
                    ),
                ),

                html.form(action=self.url(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;
}

    """

    def process (self) :
        self.process_cookie()

        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 :
                    self.pubtkt = self.app.auth(username, password)

                except pubtkt.Error as ex :
                    self.auth_errors = ex
            
            elif self.pubtkt and self.pubtkt.valid() :
                # renew
                self.app.renew(self.pubtkt)

            else :
                return

            # 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(self.pubtkt))

            # 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) :
        if self.pubtkt :
            username = self.pubtkt.uid
        else :
            username = None

        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, value=username),
                            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) :
        self.process_cookie()

        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(seconds=60)

    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)
 
    def renew (self, pubtkt) :
        """
            Renew and re-sign the given pubtkt.
        """

        pubtkt.renew(self.login_expire)