--- a/pvl/login/pubtkt.py Mon Jan 13 02:46:18 2014 +0200
+++ b/pvl/login/pubtkt.py Mon Jan 13 03:20:04 2014 +0200
@@ -14,6 +14,9 @@
return calendar.timegm(dt.utctimetuple())
+def unix2datetime (unix) :
+ return datetime.datetime.utcfromtimestamp(unix)
+
class Error (Exception) :
pass
@@ -43,6 +46,10 @@
self.private = private
class PubTkt (object) :
+ @staticmethod
+ def now () :
+ return datetime.datetime.utcnow()
+
@classmethod
def load (cls, cookie, public_key) :
"""
@@ -61,7 +68,7 @@
except M2Crypto.RSA.RSAError as ex :
raise VerifyError(pubtkt, str(ex))
- now = datetime.datetime.now()
+ now = cls.now()
log.debug("validating %s < %s", pubtkt.validuntil, now)
@@ -105,17 +112,17 @@
"""
return cls(uid,
- validuntil = datetime.datetime.fromtimestamp(int(validuntil)),
+ validuntil = unix2datetime(int(validuntil)),
cip = ipaddr.IPAddress(cip) if cip else None,
tokens = tokens.split(',') if tokens else (),
udata = udata,
- graceperiod = datetime.datetime.fromtimestamp(int(graceperiod)) if graceperiod else None,
+ graceperiod = unix2datetime(int(graceperiod)) if graceperiod else None,
bauth = bauth,
)
@classmethod
def new (cls, uid, expiry, **opts) :
- now = datetime.datetime.now()
+ now = cls.now()
return cls(uid, now + expiry, **opts)
@@ -166,12 +173,31 @@
Return remaining ticket validity.
"""
- return self.validuntil - datetime.datetime.now()
+ now = self.now()
+
+ if self.validuntil > now :
+ return self.validuntil - now
+ else :
+ return False
def grace (self) :
"""
Return remaining ticket grace.
"""
+
+ now = self.now()
+
+ if not self.graceperiod :
+ return None
- return self.graceperiod - datetime.datetime.now()
+ if self.graceperiod > now :
+ return self.graceperiod - now
+ else :
+ return False
+ def renew (self, expiry) :
+ if not self.valid() :
+ raise ExpiredError(self, "Unable to renew expired pubtkt")
+
+ self.validuntil = self.now() + expiry
+
--- a/pvl/login/server.py Mon Jan 13 02:46:18 2014 +0200
+++ b/pvl/login/server.py Mon Jan 13 03:20:04 2014 +0200
@@ -28,6 +28,10 @@
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...
@@ -37,27 +41,34 @@
cookie = self.request.cookies.get(self.app.cookie_name)
+ if not cookie :
+ return
+
log.debug("cookie %s=%s", self.app.cookie_name, cookie)
- if cookie :
- cookie = werkzeug.urls.url_unquote(cookie)
+ cookie = werkzeug.urls.url_unquote(cookie)
log.debug("cookie decoded: %s", cookie)
- if cookie :
- return self.app.load(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"
- pubtkt = None
- cookie_error = None
-
def process (self) :
- try :
- self.pubtkt = self.process_cookie()
- except pubtkt.Error as ex :
- self.cookie_error = ex
+ self.process_cookie()
if not self.pubtkt :
return self.redirect(Login)
@@ -70,46 +81,79 @@
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', "User account", pubtkt.uid
-
- valid = self.render_valid(pubtkt.valid())
+ yield 'user', None, "User account", pubtkt.uid
- if pubtkt.graceperiod :
- valid = "{valid} ({grace})".format(valid=valid, grace=self.render_valid(pubtkt.grace()))
+ valid = pubtkt.valid()
+ grace = pubtkt.grace()
- yield 'time', "Remaining validity", valid
+ 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', "Network address", pubtkt.cip
+ yield 'cloud', None, "Network address", pubtkt.cip
if pubtkt.udata :
- yield 'comment', "Associated data", pubtkt.udata
+ yield 'comment', None, "Associated data", pubtkt.udata
for token in pubtkt.tokens :
- yield 'flag', "Access token", token
+ yield 'flag', None, "Access token", token
if pubtkt.bauth :
- yield 'keys', "Authentication token", pubtkt.bauth
+ yield 'keys', None, "Authentication token", pubtkt.bauth
def render_pubtkt (self, pubtkt) :
- return html.div(class_='panel panel-info')(
+ 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.div(class_='panel-body')(
- "This is a valid login ticket.",
- ),
html.ul(class_='list-group')(
- html.li(class_='list-group-item', title=title)(
+ 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, title, info in self.render_pubtkt_fields(pubtkt)
+ ) for icon, status, title, info in self.render_pubtkt_fields(pubtkt)
),
html.div(class_='panel-footer')(
- html.form(action='/logout', method='post')(
+ (
+ 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"),
),
),
@@ -142,10 +186,9 @@
"""
+ def process (self) :
+ self.process_cookie()
- auth_error = None
-
- def process (self) :
if self.request.method == 'POST' :
back = self.app.login_server
username = self.request.form.get('username')
@@ -156,28 +199,39 @@
username = username.strip().lower()
try :
- pt = self.app.auth(username, password)
+ self.pubtkt = 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))
+
+ elif self.pubtkt and self.pubtkt.valid() :
+ # renew
+ self.app.renew(self.pubtkt)
- # redirect with cookie
- response = pvl.web.response.redirect(back)
+ else :
+ return
- response.set_cookie(self.app.cookie_name, cookie,
- domain = self.app.cookie_domain,
- secure = self.app.cookie_secure,
- httponly = self.app.cookie_httponly,
- )
+ # 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))
- return response
+ # 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')(
@@ -188,7 +242,7 @@
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.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)),
),
@@ -205,11 +259,7 @@
TITLE = "Logout"
def process (self) :
- try :
- self.pubtkt = self.process_cookie()
- except Error as ex :
- self.pubtkt_error = ex
- self.pubtkt = ex.pubtkt
+ self.process_cookie()
if not self.pubtkt :
return self.redirect(Index)
@@ -251,7 +301,7 @@
login_domain = 'test.paivola.fi'
login_server = 'https://login.test.paivola.fi/'
- login_expire = datetime.timedelta(hours=1)
+ login_expire = datetime.timedelta(seconds=60)
cookie_name = 'auth_pubtkt'
cookie_domain = 'test.paivola.fi'
@@ -289,3 +339,9 @@
return pubtkt.sign(self.server_keys.private)
+ def renew (self, pubtkt) :
+ """
+ Renew and re-sign the given pubtkt.
+ """
+
+ pubtkt.renew(self.login_expire)