Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b0e1856b5 | ||
|
|
94841c90c3 | ||
|
|
f14e5f728c | ||
|
|
8443c17839 | ||
|
|
be8114127e | ||
|
|
4eaef303ff | ||
|
|
319a3fc1dc | ||
|
|
463d643e2c | ||
|
|
0cbed12952 | ||
|
|
b5994e57a4 | ||
|
|
e239b975be | ||
|
|
0259ea3d29 | ||
|
|
db4c008de3 | ||
|
|
7921e5148a | ||
|
|
ee23eab77a | ||
|
|
2344386f77 | ||
|
|
b5e12031c5 | ||
|
|
85e6840cf0 | ||
|
|
4c3006acb4 | ||
|
|
f8f008cab5 | ||
|
|
6bff8af458 |
@@ -59,3 +59,4 @@ HOWTO-web2py-devel
|
|||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
.idea/*
|
.idea/*
|
||||||
site-packages/
|
site-packages/
|
||||||
|
logs/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
## 2.13.1
|
## 2.13.1-2
|
||||||
|
|
||||||
|
- fixed a security issue in request_reset_password
|
||||||
- added fabfile.py
|
- added fabfile.py
|
||||||
- fixed oauth2 renew token, thanks dokime7
|
- fixed oauth2 renew token, thanks dokime7
|
||||||
- fixed add_membership, del_membership, add_membership IntegrityError (when auth.enable_record_versioning)
|
- fixed add_membership, del_membership, add_membership IntegrityError (when auth.enable_record_versioning)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ update:
|
|||||||
echo "remember that pymysql was tweaked"
|
echo "remember that pymysql was tweaked"
|
||||||
src:
|
src:
|
||||||
### Use semantic versioning
|
### Use semantic versioning
|
||||||
echo 'Version 2.13.2-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
echo 'Version 2.13.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
||||||
### rm -f all junk files
|
### rm -f all junk files
|
||||||
make clean
|
make clean
|
||||||
### clean up baisc apps
|
### clean up baisc apps
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Version 2.12.3-stable+timestamp.2015.08.18.19.14.07
|
Version 2.13.3-stable+timestamp.2015.12.24.08.08.22
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ def bg_graph_model():
|
|||||||
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
||||||
|
|
||||||
group = meta_graphmodel['group'].replace(' ', '')
|
group = meta_graphmodel['group'].replace(' ', '')
|
||||||
if not subgraphs.has_key(group):
|
if group not in subgraphs:
|
||||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||||
subgraphs[group]['tables'].append(tablename)
|
subgraphs[group]['tables'].append(tablename)
|
||||||
|
|
||||||
|
|||||||
@@ -155,8 +155,8 @@
|
|||||||
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||||
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
|
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
|
||||||
</p>
|
</p>
|
||||||
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "hidden" );')}}
|
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||||
<div class="hidden" id="all_keys">
|
<div class="w2p_hidden" id="all_keys">
|
||||||
{{=total['keys']}}
|
{{=total['keys']}}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -183,8 +183,8 @@
|
|||||||
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||||
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
|
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
|
||||||
</p>
|
</p>
|
||||||
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "hidden" );')}}
|
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||||
<div class="hidden" id="ram_keys">
|
<div class="w2p_hidden" id="ram_keys">
|
||||||
{{=ram['keys']}}
|
{{=ram['keys']}}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||||
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
|
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
|
||||||
</p>
|
</p>
|
||||||
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "hidden" );')}}
|
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||||
<div class="hidden" id="disk_keys">
|
<div class="w2p_hidden" id="disk_keys">
|
||||||
{{=disk['keys']}}
|
{{=disk['keys']}}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -249,8 +249,8 @@
|
|||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
|
|||||||
{{=peekfile('views',c, dict(id=id))}}
|
{{=peekfile('views',c, dict(id=id))}}
|
||||||
</span>
|
</span>
|
||||||
<span class="extras celled celled-one">
|
<span class="extras celled celled-one">
|
||||||
{{if extend.has_key(c):}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
{{if c in extend:}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
||||||
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
|
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
|
|||||||
{{=peekfile('views',c)}}
|
{{=peekfile('views',c)}}
|
||||||
</span>
|
</span>
|
||||||
<span class="extras celled">
|
<span class="extras celled">
|
||||||
{{if extend.has_key(c):}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
{{if c in extend:}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
||||||
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
|
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ def bg_graph_model():
|
|||||||
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
||||||
|
|
||||||
group = meta_graphmodel['group'].replace(' ', '')
|
group = meta_graphmodel['group'].replace(' ', '')
|
||||||
if not subgraphs.has_key(group):
|
if group not in subgraphs:
|
||||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||||
subgraphs[group]['tables'].append(tablename)
|
subgraphs[group]['tables'].append(tablename)
|
||||||
|
|
||||||
|
|||||||
@@ -155,8 +155,8 @@
|
|||||||
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||||
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
|
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
|
||||||
</p>
|
</p>
|
||||||
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "hidden" );')}}
|
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||||
<div class="hidden" id="all_keys">
|
<div class="w2p_hidden" id="all_keys">
|
||||||
{{=total['keys']}}
|
{{=total['keys']}}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -183,8 +183,8 @@
|
|||||||
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||||
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
|
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
|
||||||
</p>
|
</p>
|
||||||
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "hidden" );')}}
|
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||||
<div class="hidden" id="ram_keys">
|
<div class="w2p_hidden" id="ram_keys">
|
||||||
{{=ram['keys']}}
|
{{=ram['keys']}}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -212,8 +212,8 @@
|
|||||||
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||||
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
|
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
|
||||||
</p>
|
</p>
|
||||||
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "hidden" );')}}
|
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||||
<div class="hidden" id="disk_keys">
|
<div class="w2p_hidden" id="disk_keys">
|
||||||
{{=disk['keys']}}
|
{{=disk['keys']}}
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -249,8 +249,8 @@
|
|||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
||||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ def bg_graph_model():
|
|||||||
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
||||||
|
|
||||||
group = meta_graphmodel['group'].replace(' ', '')
|
group = meta_graphmodel['group'].replace(' ', '')
|
||||||
if not subgraphs.has_key(group):
|
if group not in subgraphs:
|
||||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||||
subgraphs[group]['tables'].append(tablename)
|
subgraphs[group]['tables'].append(tablename)
|
||||||
|
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import datetime
|
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
from gluon.serializers import json_parser
|
|
||||||
import base64
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
from gluon.storage import Storage
|
|
||||||
from gluon.utils import web2py_uuid
|
|
||||||
from gluon import current
|
|
||||||
from gluon.http import HTTP
|
|
||||||
|
|
||||||
|
|
||||||
class Web2pyJwt(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
If left externally, this needs the usual "singleton" approach.
|
|
||||||
Given I (we) don't know if to include in auth yet, let's stick to basics.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- secret_key: the secret. Without salting, an attacker knowing this can impersonate
|
|
||||||
any user
|
|
||||||
- algorithm : uses as they are in the JWT specs, HS256, HS384 or HS512 basically means
|
|
||||||
signing with HMAC with a 256, 284 or 512bit hash
|
|
||||||
- verify_expiration : verifies the expiration checking the exp claim
|
|
||||||
- leeway: allow n seconds of skew when checking for token expiration
|
|
||||||
- expiration : how many seconds a token may be valid
|
|
||||||
- allow_refresh: enable the machinery to get a refreshed token passing a not-already-expired
|
|
||||||
token
|
|
||||||
- refresh_expiration_delta: to avoid continous refresh of the token
|
|
||||||
- header_prefix : self-explanatory. "JWT" and "Bearer" seems to be the emerging standards
|
|
||||||
- jwt_add_header: a dict holding additional mappings to the header. by default only alg and typ are filled
|
|
||||||
- user_param: the name of the parameter holding the username when requesting a token. Can be useful, e.g, for
|
|
||||||
email-based authentication, with "email" as a parameter
|
|
||||||
- pass_param: same as above, but for the password
|
|
||||||
- realm: self-explanatory
|
|
||||||
- salt: can be static or a function that takes the payload as an argument.
|
|
||||||
Example:
|
|
||||||
def mysalt(payload):
|
|
||||||
return payload['hmac_key'].split('-')[0]
|
|
||||||
- additional_payload: can be a dict to merge with the payload or a function that takes
|
|
||||||
the payload as input and returns the modified payload
|
|
||||||
Example:
|
|
||||||
def myadditional_payload(payload):
|
|
||||||
payload['my_name_is'] = 'bond,james bond'
|
|
||||||
return payload
|
|
||||||
- before_authorization: can be a callable that takes the deserialized token (a dict) as input.
|
|
||||||
Gets called right after signature verification but before the actual
|
|
||||||
authorization takes place. You can raise with HTTP a proper error message
|
|
||||||
Example:
|
|
||||||
def mybefore_authorization(tokend):
|
|
||||||
if not tokend['my_name_is'] == 'bond,james bond':
|
|
||||||
raise HTTP(400, u'Invalid JWT my_name_is claim')
|
|
||||||
- max_header_length: check max length to avoid load()ing unusually large tokens (could mean crafted, e.g. in a DDoS.)
|
|
||||||
|
|
||||||
Basic Usage:
|
|
||||||
in models (or the controller needing it)
|
|
||||||
|
|
||||||
myjwt = Web2pyJwt('secret', auth)
|
|
||||||
|
|
||||||
in the controller issuing tokens
|
|
||||||
|
|
||||||
def login_and_take_token():
|
|
||||||
return myjwt.jwt_token_manager()
|
|
||||||
|
|
||||||
A call then to /app/controller/login_and_take_token/auth with username and password returns the token
|
|
||||||
A call to /app/controller/login_and_take_token/refresh with the original token returns the refreshed token
|
|
||||||
|
|
||||||
To protect a function with JWT
|
|
||||||
|
|
||||||
@myjwt.requires_jwt()
|
|
||||||
@auth.requires_login()
|
|
||||||
def protected():
|
|
||||||
return '%s$%s' % (request.now, auth.user_id)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, secret_key,
|
|
||||||
auth,
|
|
||||||
algorithm='HS256',
|
|
||||||
verify_expiration=True,
|
|
||||||
leeway=30,
|
|
||||||
expiration=60 * 5,
|
|
||||||
allow_refresh=True,
|
|
||||||
refresh_expiration_delta=60 * 60,
|
|
||||||
header_prefix='Bearer',
|
|
||||||
jwt_add_header=None,
|
|
||||||
user_param='username',
|
|
||||||
pass_param='password',
|
|
||||||
realm='Login required',
|
|
||||||
salt=None,
|
|
||||||
additional_payload=None,
|
|
||||||
before_authorization=None,
|
|
||||||
max_header_length=4*1024,
|
|
||||||
):
|
|
||||||
self.secret_key = secret_key
|
|
||||||
self.auth = auth
|
|
||||||
self.algorithm = algorithm
|
|
||||||
if self.algorithm not in ('HS256', 'HS384', 'HS512'):
|
|
||||||
raise NotImplementedError('Algoritm %s not allowed' % algorithm)
|
|
||||||
self.verify_expiration = verify_expiration
|
|
||||||
self.leeway = leeway
|
|
||||||
self.expiration = expiration
|
|
||||||
self.allow_refresh = allow_refresh
|
|
||||||
self.refresh_expiration_delta = refresh_expiration_delta
|
|
||||||
self.header_prefix = header_prefix
|
|
||||||
self.jwt_add_header = jwt_add_header or {}
|
|
||||||
base_header = {'alg': self.algorithm, 'typ': 'JWT'}
|
|
||||||
for k, v in self.jwt_add_header.iteritems():
|
|
||||||
base_header[k] = v
|
|
||||||
self.cached_b64h = self.jwt_b64e(json_parser.dumps(base_header))
|
|
||||||
digestmod_mapping = {
|
|
||||||
'HS256': hashlib.sha256,
|
|
||||||
'HS384': hashlib.sha384,
|
|
||||||
'HS512': hashlib.sha512
|
|
||||||
}
|
|
||||||
self.digestmod = digestmod_mapping[algorithm]
|
|
||||||
self.user_param = user_param
|
|
||||||
self.pass_param = pass_param
|
|
||||||
self.realm = realm
|
|
||||||
self.salt = salt
|
|
||||||
self.additional_payload = additional_payload
|
|
||||||
self.before_authorization = before_authorization
|
|
||||||
self.max_header_length = max_header_length
|
|
||||||
print 'initialized'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def jwt_b64e(string):
|
|
||||||
if isinstance(string, unicode):
|
|
||||||
string = string.encode('uft-8', 'strict')
|
|
||||||
return base64.urlsafe_b64encode(string).strip(b'=')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def jwt_b64d(string):
|
|
||||||
"""base64 decodes a single bytestring (and is tolerant to getting
|
|
||||||
called with a unicode string).
|
|
||||||
The result is also a bytestring.
|
|
||||||
"""
|
|
||||||
if isinstance(string, unicode):
|
|
||||||
string = string.encode('ascii', 'ignore')
|
|
||||||
return base64.urlsafe_b64decode(string + '=' * (-len(string) % 4))
|
|
||||||
|
|
||||||
def generate_token(self, payload):
|
|
||||||
secret = self.secret_key
|
|
||||||
if self.salt:
|
|
||||||
if callable(self.salt):
|
|
||||||
secret = "%s$%s" % (secret, self.salt(payload))
|
|
||||||
else:
|
|
||||||
secret = "%s$%s" % (secret, self.salt)
|
|
||||||
if isinstance(secret, unicode):
|
|
||||||
secret = secret.encode('ascii', 'ignore')
|
|
||||||
b64h = self.cached_b64h
|
|
||||||
b64p = self.jwt_b64e(json_parser.dumps(payload))
|
|
||||||
jbody = b64h + '.' + b64p
|
|
||||||
mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod)
|
|
||||||
jsign = self.jwt_b64e(mauth.digest())
|
|
||||||
return jbody + '.' + jsign
|
|
||||||
|
|
||||||
def verify_signature(self, body, signature, secret):
|
|
||||||
mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod)
|
|
||||||
return hmac.compare_digest(self.jwt_b64e(mauth.digest()), signature)
|
|
||||||
|
|
||||||
def load_token(self, token):
|
|
||||||
if isinstance(token, unicode):
|
|
||||||
token = token.encode('utf-8', 'strict')
|
|
||||||
body, sig = token.rsplit('.', 1)
|
|
||||||
b64h, b64b = body.split('.', 1)
|
|
||||||
if b64h != self.cached_b64h:
|
|
||||||
# header not the same
|
|
||||||
raise HTTP(400, u'Invalid JWT Header')
|
|
||||||
secret = self.secret_key
|
|
||||||
tokend = json_parser.loads(self.jwt_b64d(b64b))
|
|
||||||
if self.salt:
|
|
||||||
if callable(self.salt):
|
|
||||||
secret = "%s$%s" % (secret, self.salt(tokend))
|
|
||||||
else:
|
|
||||||
secret = "%s$%s" % (secret, self.salt)
|
|
||||||
if isinstance(secret, unicode):
|
|
||||||
secret = secret.encode('ascii', 'ignore')
|
|
||||||
if not self.verify_signature(body, sig, secret):
|
|
||||||
# signature verification failed
|
|
||||||
raise HTTP(400, u'Token signature is invalid')
|
|
||||||
if self.verify_expiration:
|
|
||||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
|
||||||
if tokend['exp'] + self.leeway < now:
|
|
||||||
raise HTTP(400, u'Token is expired')
|
|
||||||
if callable(self.before_authorization):
|
|
||||||
self.before_authorization(tokend)
|
|
||||||
return tokend
|
|
||||||
|
|
||||||
def serialize_auth_session(self, session_auth):
|
|
||||||
"""
|
|
||||||
As bad as it sounds, as long as this is rarely used (vs using the token)
|
|
||||||
this is the faster method, even if we ditch session in jwt_token_manager().
|
|
||||||
We (mis)use the heavy default auth mechanism to avoid any further computation,
|
|
||||||
while sticking to a somewhat-stable Auth API.
|
|
||||||
"""
|
|
||||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
|
||||||
expires = now + self.expiration
|
|
||||||
payload = dict(
|
|
||||||
hmac_key=session_auth['hmac_key'],
|
|
||||||
user_groups=session_auth['user_groups'],
|
|
||||||
user=session_auth['user'].as_dict(),
|
|
||||||
iat=now,
|
|
||||||
exp=expires
|
|
||||||
)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
def refresh_token(self, orig_payload):
|
|
||||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
|
||||||
if self.verify_expiration:
|
|
||||||
orig_exp = orig_payload['exp']
|
|
||||||
if orig_exp + self.leeway < now:
|
|
||||||
# token already expired, can't be used for refresh
|
|
||||||
raise HTTP(400, u'Token already expired')
|
|
||||||
orig_iat = orig_payload.get('orig_iat') or orig_payload['iat']
|
|
||||||
if orig_iat + self.refresh_expiration_delta < now:
|
|
||||||
# refreshed too long ago
|
|
||||||
raise HTTP(400, u'Token issued too long ago')
|
|
||||||
expires = now + self.refresh_expiration_delta
|
|
||||||
orig_payload.update(
|
|
||||||
orig_iat=orig_iat,
|
|
||||||
iat=now,
|
|
||||||
exp=expires,
|
|
||||||
hmac_key=web2py_uuid()
|
|
||||||
)
|
|
||||||
self.alter_payload(orig_payload)
|
|
||||||
return orig_payload
|
|
||||||
|
|
||||||
def alter_payload(self, payload):
|
|
||||||
if self.additional_payload:
|
|
||||||
if callable(self.additional_payload):
|
|
||||||
payload = self.additional_payload(payload)
|
|
||||||
elif isinstance(self.additional_payload, dict):
|
|
||||||
payload.update(self.additional_payload)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
def jwt_token_manager(self):
|
|
||||||
"""
|
|
||||||
The part that issues (and refreshes) tokens.
|
|
||||||
Used in a controller, given myjwt is the istantiated class, as
|
|
||||||
|
|
||||||
def api_auth():
|
|
||||||
return myjwt.jwt_token_manager()
|
|
||||||
|
|
||||||
Then, a call to /app/c/api_auth/auth with username and password
|
|
||||||
returns a token, while /app/c/api_auth/refresh with the current token
|
|
||||||
issues another token
|
|
||||||
"""
|
|
||||||
request = current.request
|
|
||||||
# forget and unlock response
|
|
||||||
if request.args(0) == 'auth':
|
|
||||||
current.session.forget(current.response)
|
|
||||||
username = request.vars[self.user_param]
|
|
||||||
password = request.vars[self.pass_param]
|
|
||||||
valid_user = self.auth.login_bare(username, password)
|
|
||||||
if valid_user:
|
|
||||||
payload = self.serialize_auth_session(current.session.auth)
|
|
||||||
self.alter_payload(payload)
|
|
||||||
return self.generate_token(payload)
|
|
||||||
else:
|
|
||||||
raise HTTP(
|
|
||||||
401, u'Not Authorized',
|
|
||||||
**{'WWW-Authenticate': u'JWT realm="%s"' % self.realm})
|
|
||||||
elif request.args(0) == 'refresh':
|
|
||||||
if not self.allow_refresh:
|
|
||||||
raise HTTP(403, u'Refreshing token is not allowed')
|
|
||||||
token = request.vars.token
|
|
||||||
tokend = self.load_token(token)
|
|
||||||
# verification can fail here
|
|
||||||
refreshed = self.refresh_token(tokend)
|
|
||||||
return self.generate_token(refreshed)
|
|
||||||
|
|
||||||
def inject_token(self, tokend):
|
|
||||||
"""
|
|
||||||
The real deal, not touching the db but still logging-in the user
|
|
||||||
"""
|
|
||||||
self.auth.user = Storage(tokend['user'])
|
|
||||||
self.auth.user_groups = tokend['user_groups']
|
|
||||||
self.auth.hmac_key = tokend['hmac_key']
|
|
||||||
|
|
||||||
def requires_jwt(self, otherwise=None):
|
|
||||||
"""
|
|
||||||
The validator that checks for the header or the
|
|
||||||
_token var
|
|
||||||
"""
|
|
||||||
request = current.request
|
|
||||||
token_in_header = request.env.http_authorization
|
|
||||||
if token_in_header:
|
|
||||||
parts = token_in_header.split()
|
|
||||||
if parts[0].lower() != self.header_prefix.lower():
|
|
||||||
raise HTTP(400, u'Invalid JWT header')
|
|
||||||
elif len(parts) == 1:
|
|
||||||
raise HTTP(400, u'Invalid JWT header, missing token')
|
|
||||||
elif len(parts) > 2:
|
|
||||||
raise HTTP(400, 'Invalid JWT header, token contains spaces')
|
|
||||||
token = parts[1]
|
|
||||||
else:
|
|
||||||
token = request.vars._token
|
|
||||||
if token and len(token) < self.max_header_length:
|
|
||||||
tokend = self.load_token(token)
|
|
||||||
self.inject_token(tokend)
|
|
||||||
return self.auth.requires(True, otherwise=otherwise)
|
|
||||||
+1
-1
@@ -2646,7 +2646,7 @@ def test():
|
|||||||
>>> form=FORM(INPUT(value="Hello World", _name="var", requires=IS_MATCH('^\w+$')))
|
>>> form=FORM(INPUT(value="Hello World", _name="var", requires=IS_MATCH('^\w+$')))
|
||||||
>>> isinstance(form.as_dict(), dict)
|
>>> isinstance(form.as_dict(), dict)
|
||||||
True
|
True
|
||||||
>>> form.as_dict(flat=True).has_key("vars")
|
>>> "vars" in form.as_dict(flat=True)
|
||||||
True
|
True
|
||||||
>>> isinstance(form.as_json(), basestring) and len(form.as_json(sanitize=False)) > 0
|
>>> isinstance(form.as_json(), basestring) and len(form.as_json(sanitize=False)) > 0
|
||||||
True
|
True
|
||||||
|
|||||||
+1
-1
Submodule gluon/packages/dal updated: 1d8ac4f562...dcfb5f58aa
@@ -1679,9 +1679,6 @@ class SQLFORM(FORM):
|
|||||||
elif field.type == 'double':
|
elif field.type == 'double':
|
||||||
if value is not None:
|
if value is not None:
|
||||||
fields[fieldname] = safe_float(value)
|
fields[fieldname] = safe_float(value)
|
||||||
elif field.type in ('string', 'text'):
|
|
||||||
if fieldname in self.request_vars:
|
|
||||||
fields[fieldname] = self.request_vars[fieldname]
|
|
||||||
|
|
||||||
for fieldname in self.vars:
|
for fieldname in self.vars:
|
||||||
if fieldname != 'id' and fieldname in self.table.fields\
|
if fieldname != 'id' and fieldname in self.table.fields\
|
||||||
|
|||||||
+359
-5
@@ -32,8 +32,12 @@ import cStringIO
|
|||||||
import ConfigParser
|
import ConfigParser
|
||||||
import email.utils
|
import email.utils
|
||||||
import random
|
import random
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset
|
from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset
|
||||||
|
|
||||||
|
from gluon.serializers import json_parser
|
||||||
from gluon.contenttype import contenttype
|
from gluon.contenttype import contenttype
|
||||||
from gluon.storage import Storage, StorageList, Settings, Messages
|
from gluon.storage import Storage, StorageList, Settings, Messages
|
||||||
from gluon.utils import web2py_uuid
|
from gluon.utils import web2py_uuid
|
||||||
@@ -1136,6 +1140,304 @@ def addrow(form, a, b, c, style, _id, position=-1):
|
|||||||
TD(c, _class='w2p_fc'), _id=_id))
|
TD(c, _class='w2p_fc'), _id=_id))
|
||||||
|
|
||||||
|
|
||||||
|
class AuthJWT(object):
|
||||||
|
|
||||||
|
"""
|
||||||
|
If left externally, this needs the usual "singleton" approach.
|
||||||
|
Given I (we) don't know if to include in auth yet, let's stick to basics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- secret_key: the secret. Without salting, an attacker knowing this can impersonate
|
||||||
|
any user
|
||||||
|
- algorithm : uses as they are in the JWT specs, HS256, HS384 or HS512 basically means
|
||||||
|
signing with HMAC with a 256, 284 or 512bit hash
|
||||||
|
- verify_expiration : verifies the expiration checking the exp claim
|
||||||
|
- leeway: allow n seconds of skew when checking for token expiration
|
||||||
|
- expiration : how many seconds a token may be valid
|
||||||
|
- allow_refresh: enable the machinery to get a refreshed token passing a not-already-expired
|
||||||
|
token
|
||||||
|
- refresh_expiration_delta: to avoid continous refresh of the token
|
||||||
|
- header_prefix : self-explanatory. "JWT" and "Bearer" seems to be the emerging standards
|
||||||
|
- jwt_add_header: a dict holding additional mappings to the header. by default only alg and typ are filled
|
||||||
|
- user_param: the name of the parameter holding the username when requesting a token. Can be useful, e.g, for
|
||||||
|
email-based authentication, with "email" as a parameter
|
||||||
|
- pass_param: same as above, but for the password
|
||||||
|
- realm: self-explanatory
|
||||||
|
- salt: can be static or a function that takes the payload as an argument.
|
||||||
|
Example:
|
||||||
|
def mysalt(payload):
|
||||||
|
return payload['hmac_key'].split('-')[0]
|
||||||
|
- additional_payload: can be a dict to merge with the payload or a function that takes
|
||||||
|
the payload as input and returns the modified payload
|
||||||
|
Example:
|
||||||
|
def myadditional_payload(payload):
|
||||||
|
payload['my_name_is'] = 'bond,james bond'
|
||||||
|
return payload
|
||||||
|
- before_authorization: can be a callable that takes the deserialized token (a dict) as input.
|
||||||
|
Gets called right after signature verification but before the actual
|
||||||
|
authorization takes place. You can raise with HTTP a proper error message
|
||||||
|
Example:
|
||||||
|
def mybefore_authorization(tokend):
|
||||||
|
if not tokend['my_name_is'] == 'bond,james bond':
|
||||||
|
raise HTTP(400, u'Invalid JWT my_name_is claim')
|
||||||
|
- max_header_length: check max length to avoid load()ing unusually large tokens (could mean crafted, e.g. in a DDoS.)
|
||||||
|
|
||||||
|
Basic Usage:
|
||||||
|
in models (or the controller needing it)
|
||||||
|
|
||||||
|
myjwt = AuthJWT(auth, secret_key='secret')
|
||||||
|
|
||||||
|
in the controller issuing tokens
|
||||||
|
|
||||||
|
def login_and_take_token():
|
||||||
|
return myjwt.jwt_token_manager()
|
||||||
|
|
||||||
|
A call then to /app/controller/login_and_take_token/auth with username and password returns the token
|
||||||
|
A call to /app/controller/login_and_take_token/refresh with the original token returns the refreshed token
|
||||||
|
|
||||||
|
To protect a function with JWT
|
||||||
|
|
||||||
|
@myjwt.allows_jwt()
|
||||||
|
@auth.requires_login()
|
||||||
|
def protected():
|
||||||
|
return '%s$%s' % (request.now, auth.user_id)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
auth,
|
||||||
|
secret_key,
|
||||||
|
algorithm='HS256',
|
||||||
|
verify_expiration=True,
|
||||||
|
leeway=30,
|
||||||
|
expiration=60 * 5,
|
||||||
|
allow_refresh=True,
|
||||||
|
refresh_expiration_delta=60 * 60,
|
||||||
|
header_prefix='Bearer',
|
||||||
|
jwt_add_header=None,
|
||||||
|
user_param='username',
|
||||||
|
pass_param='password',
|
||||||
|
realm='Login required',
|
||||||
|
salt=None,
|
||||||
|
additional_payload=None,
|
||||||
|
before_authorization=None,
|
||||||
|
max_header_length=4*1024,
|
||||||
|
):
|
||||||
|
self.secret_key = secret_key
|
||||||
|
self.auth = auth
|
||||||
|
self.algorithm = algorithm
|
||||||
|
if self.algorithm not in ('HS256', 'HS384', 'HS512'):
|
||||||
|
raise NotImplementedError('Algoritm %s not allowed' % algorithm)
|
||||||
|
self.verify_expiration = verify_expiration
|
||||||
|
self.leeway = leeway
|
||||||
|
self.expiration = expiration
|
||||||
|
self.allow_refresh = allow_refresh
|
||||||
|
self.refresh_expiration_delta = refresh_expiration_delta
|
||||||
|
self.header_prefix = header_prefix
|
||||||
|
self.jwt_add_header = jwt_add_header or {}
|
||||||
|
base_header = {'alg': self.algorithm, 'typ': 'JWT'}
|
||||||
|
for k, v in self.jwt_add_header.iteritems():
|
||||||
|
base_header[k] = v
|
||||||
|
self.cached_b64h = self.jwt_b64e(json_parser.dumps(base_header))
|
||||||
|
digestmod_mapping = {
|
||||||
|
'HS256': hashlib.sha256,
|
||||||
|
'HS384': hashlib.sha384,
|
||||||
|
'HS512': hashlib.sha512
|
||||||
|
}
|
||||||
|
self.digestmod = digestmod_mapping[algorithm]
|
||||||
|
self.user_param = user_param
|
||||||
|
self.pass_param = pass_param
|
||||||
|
self.realm = realm
|
||||||
|
self.salt = salt
|
||||||
|
self.additional_payload = additional_payload
|
||||||
|
self.before_authorization = before_authorization
|
||||||
|
self.max_header_length = max_header_length
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def jwt_b64e(string):
|
||||||
|
if isinstance(string, unicode):
|
||||||
|
string = string.encode('uft-8', 'strict')
|
||||||
|
return base64.urlsafe_b64encode(string).strip(b'=')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def jwt_b64d(string):
|
||||||
|
"""base64 decodes a single bytestring (and is tolerant to getting
|
||||||
|
called with a unicode string).
|
||||||
|
The result is also a bytestring.
|
||||||
|
"""
|
||||||
|
if isinstance(string, unicode):
|
||||||
|
string = string.encode('ascii', 'ignore')
|
||||||
|
return base64.urlsafe_b64decode(string + '=' * (-len(string) % 4))
|
||||||
|
|
||||||
|
def generate_token(self, payload):
|
||||||
|
secret = self.secret_key
|
||||||
|
if self.salt:
|
||||||
|
if callable(self.salt):
|
||||||
|
secret = "%s$%s" % (secret, self.salt(payload))
|
||||||
|
else:
|
||||||
|
secret = "%s$%s" % (secret, self.salt)
|
||||||
|
if isinstance(secret, unicode):
|
||||||
|
secret = secret.encode('ascii', 'ignore')
|
||||||
|
b64h = self.cached_b64h
|
||||||
|
b64p = self.jwt_b64e(json_parser.dumps(payload))
|
||||||
|
jbody = b64h + '.' + b64p
|
||||||
|
mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod)
|
||||||
|
jsign = self.jwt_b64e(mauth.digest())
|
||||||
|
return jbody + '.' + jsign
|
||||||
|
|
||||||
|
def verify_signature(self, body, signature, secret):
|
||||||
|
mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod)
|
||||||
|
return hmac.compare_digest(self.jwt_b64e(mauth.digest()), signature)
|
||||||
|
|
||||||
|
def load_token(self, token):
|
||||||
|
if isinstance(token, unicode):
|
||||||
|
token = token.encode('utf-8', 'strict')
|
||||||
|
body, sig = token.rsplit('.', 1)
|
||||||
|
b64h, b64b = body.split('.', 1)
|
||||||
|
if b64h != self.cached_b64h:
|
||||||
|
# header not the same
|
||||||
|
raise HTTP(400, u'Invalid JWT Header')
|
||||||
|
secret = self.secret_key
|
||||||
|
tokend = json_parser.loads(self.jwt_b64d(b64b))
|
||||||
|
if self.salt:
|
||||||
|
if callable(self.salt):
|
||||||
|
secret = "%s$%s" % (secret, self.salt(tokend))
|
||||||
|
else:
|
||||||
|
secret = "%s$%s" % (secret, self.salt)
|
||||||
|
if isinstance(secret, unicode):
|
||||||
|
secret = secret.encode('ascii', 'ignore')
|
||||||
|
if not self.verify_signature(body, sig, secret):
|
||||||
|
# signature verification failed
|
||||||
|
raise HTTP(400, u'Token signature is invalid')
|
||||||
|
if self.verify_expiration:
|
||||||
|
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||||
|
if tokend['exp'] + self.leeway < now:
|
||||||
|
raise HTTP(400, u'Token is expired')
|
||||||
|
if callable(self.before_authorization):
|
||||||
|
self.before_authorization(tokend)
|
||||||
|
return tokend
|
||||||
|
|
||||||
|
def serialize_auth_session(self, session_auth):
|
||||||
|
"""
|
||||||
|
As bad as it sounds, as long as this is rarely used (vs using the token)
|
||||||
|
this is the faster method, even if we ditch session in jwt_token_manager().
|
||||||
|
We (mis)use the heavy default auth mechanism to avoid any further computation,
|
||||||
|
while sticking to a somewhat-stable Auth API.
|
||||||
|
"""
|
||||||
|
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||||
|
expires = now + self.expiration
|
||||||
|
payload = dict(
|
||||||
|
hmac_key=session_auth['hmac_key'],
|
||||||
|
user_groups=session_auth['user_groups'],
|
||||||
|
user=session_auth['user'].as_dict(),
|
||||||
|
iat=now,
|
||||||
|
exp=expires
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def refresh_token(self, orig_payload):
|
||||||
|
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||||
|
if self.verify_expiration:
|
||||||
|
orig_exp = orig_payload['exp']
|
||||||
|
if orig_exp + self.leeway < now:
|
||||||
|
# token already expired, can't be used for refresh
|
||||||
|
raise HTTP(400, u'Token already expired')
|
||||||
|
orig_iat = orig_payload.get('orig_iat') or orig_payload['iat']
|
||||||
|
if orig_iat + self.refresh_expiration_delta < now:
|
||||||
|
# refreshed too long ago
|
||||||
|
raise HTTP(400, u'Token issued too long ago')
|
||||||
|
expires = now + self.refresh_expiration_delta
|
||||||
|
orig_payload.update(
|
||||||
|
orig_iat=orig_iat,
|
||||||
|
iat=now,
|
||||||
|
exp=expires,
|
||||||
|
hmac_key=web2py_uuid()
|
||||||
|
)
|
||||||
|
self.alter_payload(orig_payload)
|
||||||
|
return orig_payload
|
||||||
|
|
||||||
|
def alter_payload(self, payload):
|
||||||
|
if self.additional_payload:
|
||||||
|
if callable(self.additional_payload):
|
||||||
|
payload = self.additional_payload(payload)
|
||||||
|
elif isinstance(self.additional_payload, dict):
|
||||||
|
payload.update(self.additional_payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def jwt_token_manager(self):
|
||||||
|
"""
|
||||||
|
The part that issues (and refreshes) tokens.
|
||||||
|
Used in a controller, given myjwt is the istantiated class, as
|
||||||
|
|
||||||
|
def api_auth():
|
||||||
|
return myjwt.jwt_token_manager()
|
||||||
|
|
||||||
|
Then, a call to /app/c/api_auth/auth with username and password
|
||||||
|
returns a token, while /app/c/api_auth/refresh with the current token
|
||||||
|
issues another token
|
||||||
|
"""
|
||||||
|
request = current.request
|
||||||
|
response = current.response
|
||||||
|
session = current.session
|
||||||
|
# forget and unlock response
|
||||||
|
if request.vars.token:
|
||||||
|
if not self.allow_refresh:
|
||||||
|
raise HTTP(403, u'Refreshing token is not allowed')
|
||||||
|
token = request.vars.token
|
||||||
|
tokend = self.load_token(token)
|
||||||
|
# verification can fail here
|
||||||
|
refreshed = self.refresh_token(tokend)
|
||||||
|
ret = {'token':self.generate_token(refreshed)}
|
||||||
|
elif self.user_param in request.vars and self.pass_param in request.vars:
|
||||||
|
session.forget(response)
|
||||||
|
username = request.vars[self.user_param]
|
||||||
|
password = request.vars[self.pass_param]
|
||||||
|
valid_user = self.auth.login_bare(username, password)
|
||||||
|
if valid_user:
|
||||||
|
payload = self.serialize_auth_session(current.session.auth)
|
||||||
|
self.alter_payload(payload)
|
||||||
|
ret = {'token':self.generate_token(payload)}
|
||||||
|
else:
|
||||||
|
raise HTTP(
|
||||||
|
401, u'Not Authorized',
|
||||||
|
**{'WWW-Authenticate': u'JWT realm="%s"' % self.realm})
|
||||||
|
else:
|
||||||
|
raise HTTP(400, u'Must pass token for refresh or username and password for login')
|
||||||
|
response.headers['content-type'] = 'application/json'
|
||||||
|
return json.dumps(ret)
|
||||||
|
|
||||||
|
def inject_token(self, tokend):
|
||||||
|
"""
|
||||||
|
The real deal, not touching the db but still logging-in the user
|
||||||
|
"""
|
||||||
|
self.auth.user = Storage(tokend['user'])
|
||||||
|
self.auth.user_groups = tokend['user_groups']
|
||||||
|
self.auth.hmac_key = tokend['hmac_key']
|
||||||
|
|
||||||
|
def allows_jwt(self, otherwise=None):
|
||||||
|
"""
|
||||||
|
The validator that checks for the header or the
|
||||||
|
_token var
|
||||||
|
"""
|
||||||
|
request = current.request
|
||||||
|
token_in_header = request.env.http_authorization
|
||||||
|
if token_in_header:
|
||||||
|
parts = token_in_header.split()
|
||||||
|
if parts[0].lower() != self.header_prefix.lower():
|
||||||
|
raise HTTP(400, u'Invalid JWT header')
|
||||||
|
elif len(parts) == 1:
|
||||||
|
raise HTTP(400, u'Invalid JWT header, missing token')
|
||||||
|
elif len(parts) > 2:
|
||||||
|
raise HTTP(400, 'Invalid JWT header, token contains spaces')
|
||||||
|
token = parts[1]
|
||||||
|
else:
|
||||||
|
token = request.vars._token
|
||||||
|
if token and len(token) < self.max_header_length:
|
||||||
|
tokend = self.load_token(token)
|
||||||
|
self.inject_token(tokend)
|
||||||
|
return self.auth.requires(True, otherwise=otherwise)
|
||||||
|
|
||||||
|
|
||||||
class Auth(object):
|
class Auth(object):
|
||||||
|
|
||||||
default_settings = dict(
|
default_settings = dict(
|
||||||
@@ -1424,7 +1726,7 @@ class Auth(object):
|
|||||||
hmac_key=None, controller='default', function='user',
|
hmac_key=None, controller='default', function='user',
|
||||||
cas_provider=None, signature=True, secure=False,
|
cas_provider=None, signature=True, secure=False,
|
||||||
csrf_prevention=True, propagate_extension=None,
|
csrf_prevention=True, propagate_extension=None,
|
||||||
url_index=None):
|
url_index=None, jwt=None):
|
||||||
|
|
||||||
## next two lines for backward compatibility
|
## next two lines for backward compatibility
|
||||||
if not db and environment and isinstance(environment, DAL):
|
if not db and environment and isinstance(environment, DAL):
|
||||||
@@ -1544,6 +1846,8 @@ class Auth(object):
|
|||||||
self.define_signature()
|
self.define_signature()
|
||||||
else:
|
else:
|
||||||
self.signature = None
|
self.signature = None
|
||||||
|
|
||||||
|
self.jwt_handler = jwt and AuthJWT(self, **jwt)
|
||||||
|
|
||||||
def get_vars_next(self):
|
def get_vars_next(self):
|
||||||
next = current.request.vars._next
|
next = current.request.vars._next
|
||||||
@@ -1612,7 +1916,7 @@ class Auth(object):
|
|||||||
'reset_password', 'request_reset_password',
|
'reset_password', 'request_reset_password',
|
||||||
'change_password', 'profile', 'groups',
|
'change_password', 'profile', 'groups',
|
||||||
'impersonate', 'not_authorized', 'confirm_registration',
|
'impersonate', 'not_authorized', 'confirm_registration',
|
||||||
'bulk_register','manage_tokens'):
|
'bulk_register','manage_tokens','jwt'):
|
||||||
if len(request.args) >= 2 and args[0] == 'impersonate':
|
if len(request.args) >= 2 and args[0] == 'impersonate':
|
||||||
return getattr(self, args[0])(request.args[1])
|
return getattr(self, args[0])(request.args[1])
|
||||||
else:
|
else:
|
||||||
@@ -1627,7 +1931,7 @@ class Auth(object):
|
|||||||
elif args(1) == self.settings.cas_actions['proxyvalidate']:
|
elif args(1) == self.settings.cas_actions['proxyvalidate']:
|
||||||
return self.cas_validate(version=2, proxy=True)
|
return self.cas_validate(version=2, proxy=True)
|
||||||
elif args(1) == self.settings.cas_actions['logout']:
|
elif args(1) == self.settings.cas_actions['logout']:
|
||||||
return self.logout(next=request.vars.service or DEFAULT)
|
return self.logout(next=request.vars.service or DEFAULT)
|
||||||
else:
|
else:
|
||||||
raise HTTP(404)
|
raise HTTP(404)
|
||||||
|
|
||||||
@@ -3738,6 +4042,50 @@ class Auth(object):
|
|||||||
for callback in onaccept:
|
for callback in onaccept:
|
||||||
callback(form)
|
callback(form)
|
||||||
|
|
||||||
|
def jwt(self):
|
||||||
|
"""
|
||||||
|
To use JWT authentication:
|
||||||
|
1) instantiate auth with
|
||||||
|
|
||||||
|
auth = Auth(db, jwt = {'secret_key':'secret'})
|
||||||
|
|
||||||
|
where 'secret' is your own secret string.
|
||||||
|
|
||||||
|
2) Secorate functions that require login but should accept the JWT token credentials:
|
||||||
|
|
||||||
|
@auth.allows_jwt()
|
||||||
|
@auth.requires_login()
|
||||||
|
def myapi(): return 'hello %s' % auth.user.email
|
||||||
|
|
||||||
|
Notice jwt is allowed but not required. if user is logged in, myapi is accessible.
|
||||||
|
|
||||||
|
3) Use it!
|
||||||
|
|
||||||
|
Now API users can obtain a token with
|
||||||
|
|
||||||
|
http://.../app/default/user/jwt?username=...&password=....
|
||||||
|
|
||||||
|
(returns json object with a token attribute)
|
||||||
|
API users can refresh an existing token with
|
||||||
|
|
||||||
|
http://.../app/default/user/jwt?token=...
|
||||||
|
|
||||||
|
they can authenticate themselves when calling http:/.../myapi by injecting a header
|
||||||
|
|
||||||
|
Authorization: Bearer <the jwt token>
|
||||||
|
|
||||||
|
Any additional attributes in the jwt argument of Auth() below:
|
||||||
|
|
||||||
|
auth = Auth(db, jwt = {...})
|
||||||
|
|
||||||
|
are passed to the constructor of class AuthJWT. Look there for documentation.
|
||||||
|
"""
|
||||||
|
if not self.jwt_handler:
|
||||||
|
raise HTTP(400, "Not authorized")
|
||||||
|
else:
|
||||||
|
current.response.headers['content-type'] = 'application/json'
|
||||||
|
raise HTTP(200, self.jwt_handler.jwt_token_manager())
|
||||||
|
|
||||||
def is_impersonating(self):
|
def is_impersonating(self):
|
||||||
return self.is_logged_in() and 'impersonator' in current.session.auth
|
return self.is_logged_in() and 'impersonator' in current.session.auth
|
||||||
|
|
||||||
@@ -3836,6 +4184,12 @@ class Auth(object):
|
|||||||
raise HTTP(403, 'ACCESS DENIED')
|
raise HTTP(403, 'ACCESS DENIED')
|
||||||
return self.messages.access_denied
|
return self.messages.access_denied
|
||||||
|
|
||||||
|
def allows_jwt(self, otherwise=None):
|
||||||
|
if not self.jwt_handler:
|
||||||
|
raise HTTP(400, "Not authorized")
|
||||||
|
else:
|
||||||
|
return self.jwt_handler.allows_jwt()
|
||||||
|
|
||||||
def requires(self, condition, requires_login=True, otherwise=None):
|
def requires(self, condition, requires_login=True, otherwise=None):
|
||||||
"""
|
"""
|
||||||
Decorator that prevents access to action if not logged in
|
Decorator that prevents access to action if not logged in
|
||||||
@@ -4339,7 +4693,7 @@ class Auth(object):
|
|||||||
if resolve:
|
if resolve:
|
||||||
if slug:
|
if slug:
|
||||||
wiki = self._wiki.read(slug, force_render)
|
wiki = self._wiki.read(slug, force_render)
|
||||||
if isinstance(wiki, dict) and wiki.has_key('content'): # FIXME: .has_key() is deprecated
|
if isinstance(wiki, dict) and 'content' in wiki:
|
||||||
# We don't want to return a dict object, just the wiki
|
# We don't want to return a dict object, just the wiki
|
||||||
wiki = wiki['content']
|
wiki = wiki['content']
|
||||||
else:
|
else:
|
||||||
@@ -5356,7 +5710,7 @@ class Service(object):
|
|||||||
|
|
||||||
def return_error(id, code, message=None, data=None):
|
def return_error(id, code, message=None, data=None):
|
||||||
error = {'code': code}
|
error = {'code': code}
|
||||||
if Service.jsonrpc_errors.has_key(code):
|
if code in Service.jsonrpc_errors:
|
||||||
error['message'] = Service.jsonrpc_errors[code][0]
|
error['message'] = Service.jsonrpc_errors[code][0]
|
||||||
error['data'] = Service.jsonrpc_errors[code][1]
|
error['data'] = Service.jsonrpc_errors[code][1]
|
||||||
if message is not None:
|
if message is not None:
|
||||||
|
|||||||
+7
-7
@@ -40,8 +40,8 @@ ProgramInfo = '''%s
|
|||||||
%s
|
%s
|
||||||
%s''' % (ProgramName, ProgramAuthor, ProgramVersion)
|
%s''' % (ProgramName, ProgramAuthor, ProgramVersion)
|
||||||
|
|
||||||
if not sys.version[:3] in ['2.5', '2.6', '2.7']:
|
if not sys.version[:3] in ['2.6', '2.7']:
|
||||||
msg = 'Warning: web2py requires Python 2.5, 2.6 or 2.7 but you are running:\n%s'
|
msg = 'Warning: web2py requires Python 2.6 or 2.7 but you are running:\n%s'
|
||||||
msg = msg % sys.version
|
msg = msg % sys.version
|
||||||
sys.stderr.write(msg)
|
sys.stderr.write(msg)
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ def run_system_tests(options):
|
|||||||
major_version = sys.version_info[0]
|
major_version = sys.version_info[0]
|
||||||
minor_version = sys.version_info[1]
|
minor_version = sys.version_info[1]
|
||||||
if major_version == 2:
|
if major_version == 2:
|
||||||
if minor_version in (5, 6):
|
if minor_version in (6,):
|
||||||
sys.stderr.write("Python 2.5 or 2.6\n")
|
sys.stderr.write('Python 2.6\n')
|
||||||
ret = subprocess.call(['unit2', '-v', 'gluon.tests'])
|
ret = subprocess.call(['unit2', '-v', 'gluon.tests'])
|
||||||
elif minor_version in (7,):
|
elif minor_version in (7,):
|
||||||
call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests']
|
call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests']
|
||||||
@@ -1117,12 +1117,12 @@ def start(cron=True):
|
|||||||
if hasattr(options, key):
|
if hasattr(options, key):
|
||||||
setattr(options, key, getattr(options2, key))
|
setattr(options, key, getattr(options2, key))
|
||||||
|
|
||||||
logfile0 = os.path.join('extras', 'examples', 'logging.example.conf')
|
logfile0 = os.path.join('examples', 'logging.example.conf')
|
||||||
if not os.path.exists('logging.conf') and os.path.exists(logfile0):
|
if not os.path.exists('logging.conf') and os.path.exists(logfile0):
|
||||||
import shutil
|
import shutil
|
||||||
sys.stdout.write("Copying logging.conf.example to logging.conf ... ")
|
sys.stdout.write("Copying logging.conf.example to logging.conf ... ")
|
||||||
shutil.copyfile('logging.example.conf', logfile0)
|
shutil.copyfile(logfile0, 'logging.conf')
|
||||||
sys.stdout.write("OK\n")
|
sys.stdout.write('OK\n')
|
||||||
|
|
||||||
# ## if -T run doctests (no cron)
|
# ## if -T run doctests (no cron)
|
||||||
if hasattr(options, 'test') and options.test:
|
if hasattr(options, 'test') and options.test:
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import gluon.widget
|
||||||
|
from multiprocessing import freeze_support
|
||||||
|
# import gluon.import_all ##### This should be uncommented for py2exe.py
|
||||||
|
|
||||||
if hasattr(sys, 'frozen'):
|
if hasattr(sys, 'frozen'):
|
||||||
path = os.path.dirname(os.path.abspath(sys.executable)) # for py2exe
|
path = os.path.dirname(os.path.abspath(sys.executable)) # for py2exe
|
||||||
@@ -14,17 +17,10 @@ os.chdir(path)
|
|||||||
|
|
||||||
sys.path = [path] + [p for p in sys.path if not p == path]
|
sys.path = [path] + [p for p in sys.path if not p == path]
|
||||||
|
|
||||||
# import gluon.import_all ##### This should be uncommented for py2exe.py
|
|
||||||
import gluon.widget
|
|
||||||
|
|
||||||
# Start Web2py and Web2py cron service!
|
# Start Web2py and Web2py cron service!
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
try:
|
freeze_support()
|
||||||
from multiprocessing import freeze_support
|
if 'COVERAGE_PROCESS_START' in os.environ:
|
||||||
freeze_support()
|
|
||||||
except:
|
|
||||||
sys.stderr.write('Sorry, -K only supported for python 2.6-2.7\n')
|
|
||||||
if os.environ.has_key("COVERAGE_PROCESS_START"):
|
|
||||||
try:
|
try:
|
||||||
import coverage
|
import coverage
|
||||||
coverage.process_startup()
|
coverage.process_startup()
|
||||||
|
|||||||
Reference in New Issue
Block a user