# 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)