New feature in trunk: API tokens

345 views
Skip to first unread message

Massimo Di Pierro

unread,
Jun 28, 2015, 10:56:35 AM6/28/15
to web...@googlegroups.com
There is a new feature in trunk. Support for native API tokens.

To enable in models/db.py use:

auth.define_tables(username=False, signature=False, api_tokens=True)


Then where appropriate replace 

@auth.requires_login()

def test(): return 'hello %s' % auth.user.first_name

with

@auth.requires_login_or_token()

def test(): return 'hello %s' % auth.user.first_name


Now your users can go to

http://..../welcome/default/user/manage_tokens

create and expire tokens and call the decorated functions with

http://..../welcome/default/test?_token=<one-of-the-tokens>


The token will give access to the function (test in the example) as if the user were logged in.

This will make it easier for you to create API for your app and delegate to your users the job of creating and expiring their tokens.


This is an EXPERIMENTAL feature. It works but it may change.


Please test, and submit comments/suggestions.


Massimo

Niphlod

unread,
Jun 28, 2015, 4:52:48 PM6/28/15
to web...@googlegroups.com
IMHO token should not be restricted to a var... it should be also possible to use an header...

Performance-wise...as it is it's as bad as it could possibly be.
1) the extra table needs a solid unique=True on the token field: we don't want to do a full scan of that table for every request protected by the decorator...and we protect ourselves from colliding uuids.
2) why do we need 2 separate queries (one for the token, the other for the id ) ?

Moreover, the grid embedded in manage_tokens is a quick - but dirty - way to manage those... There's no restrictions on the tokens that are generated (the whole table is exposed). Plus, it lacks any possibility of customization: given the abnormal abundance of arguments we added to grid initialization to accomodate zillions of users, I'd say it will pop up soon enough the need of passing something to that grid.
We should expose an API to manage tokens, not a grid...

Niphlod

unread,
Jun 28, 2015, 4:57:45 PM6/28/15
to web...@googlegroups.com
PS: the code doesn't take into consideration an expired token. the mere existance of the record allows the authentication.

Niphlod

unread,
Jun 28, 2015, 5:36:24 PM6/28/15
to web...@googlegroups.com
Ok, read it carefully....All of that IMHO isn't really what users wants to implement a token-based auth on top of an API.
Apart from the fact that if the scheme of the tokens table gets corrected (a FK to the user_id and token unique) the first two queries are collapsible into one, calling login_user() triggers update_groups() (not sure if an API needs it for every call), plus it renews the session (that in an API isn't there for sure), and creates a new Session (and again, the cookie-based Session isn't something an API uses or requires)

The way I see it, a token-based authentication is good for a kind of cached/speedy authentication. You expect zillion calls to an API and you don't want username/passwords flying around, so you publish something behind the usual auth that generates a code what identifies you. Or you have a zillions mini-programs that needs to call the api and you don't want to store username-password combo in each and every program, so you request a token. Usually the token is also generated for a "scope", so, e.g., the authenticated user with the token can't invalidate all other tokens...of change the profile email, etc etc. This goes beyond the scope of a simple helper in web2py and goes towards being an oauth provider.... an entire different story.
Let's assume though that the token auth gets the same permissions as the usual one... who you are (when you generated the token) is only one piece of the info: the other piece is what you're allowed to do with that token.
For all intents and purposes, a "token-based" auth for an API IMHO resembles very closely what in non-API environments is persisted in web2py with the Session (under the hook a cookie with the sesson id). Specifically the session.auth part.

non-api: You login, the "heavy auth thingies" take place there and only there (are you a valid user, is your password correct, which groups are you in, did you complete the registration process, and so on), and from there on you are issued a "lightweight" Session that relieves web2py from constantly checking at every request who you are and what you can do (through membership)

api: you request a token for your login, the "heavy auth thingies" take place there and only there, and you're issued a "lightweight" token (with an optional expiration) that relieves web2py from costantly checking who you are and what can you do. 

This naming (api_tokens) collides with the implementation: it's slower than the default (thinking about basic auth) because it requires at least 3 queries for any call (1) is there a token, 2) is there a user with that id (duh?), are there groups for that user). On top of that, it creates a new session for every call. As it is, it'd better implemented as a new login method (and BTW, it surely is just a login method), living in contrib.



Massimo Di Pierro

unread,
Jun 28, 2015, 6:08:00 PM6/28/15
to web...@googlegroups.com, nip...@gmail.com
as you suggested I added unique=True
as you suggested I reduced the number of db queries from 2 to 1 (when not on GAE)
yes it should check for expiration (will add that)
as you suggested you can now use a header (web2py_api_token) instead of ?_token=...
I think the manage_tokens page is useful so I will leave it there. 

I also agree with you that this is more like a login method except that it only works for decorated actions so developer can choose where to allow this. It does not have to create a session but it may. I would recommend using session.forget() within the decorated actions but I do not think it should be default. I can see programs that may want a session to be created.

I am happy to change api_tokens name with something else. What do you suggest?

Massimo

Jason (spot) Brower

unread,
Jun 29, 2015, 1:47:00 AM6/29/15
to web...@googlegroups.com, nip...@gmail.com

--
Resources:
- http://web2py.com
- http://web2py.com/book (Documentation)
- http://github.com/web2py/web2py (Source code)
- https://code.google.com/p/web2py/issues/list (Report Issues)
---
You received this message because you are subscribed to the Google Groups "web2py-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to web2py+un...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

pa...@cancamusa.net

unread,
Jun 29, 2015, 10:25:35 AM6/29/15
to web...@googlegroups.com, nip...@gmail.com
Hello:

I'm actually interested in a way to send user notification emails with custom links, so that a click sends the user to the website and also logs the user in automatically.

Is that what these tokens are for? If they are, I don't see why I would use the decorator requires_login some times, and requires_login_or_token other times.

But I'm afraid this is not their goal, is it?

Massimo Di Pierro

unread,
Jun 29, 2015, 11:32:23 AM6/29/15
to web...@googlegroups.com, pa...@cancamusa.net, nip...@gmail.com
You can use these tokens for your purpose. You would also have to do (in models/db.py)

   auth.requires_login = auth.requires_login_or_token

In the general case the user many want to allow the token only on some API so we want to distinguish.

Massimo

Pablo Angulo

unread,
Jun 29, 2015, 1:01:36 PM6/29/15
to web...@googlegroups.com

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

ok, thanks!

But I have two questions:
1. what can I do with auth.requires_membership and auth.requires(custom_function), which I use a lot?
2. so if I have to send an email, all I have to do is find the use token in the database with:

user_token = db.auth_token(user_id).token

and then use that in the url:

body = template%dict(link=URL(c=controller, f=function, vars=dict(a=1,b=2, token=user_token)))

is that correct? If it is, it's awesome!

El 29/06/15 a las 17:32, Massimo Di Pierro escribió:
> --
> Resources:
> - http://web2py.com
> - http://web2py.com/book (Documentation)
> - http://github.com/web2py/web2py (Source code)
> - https://code.google.com/p/web2py/issues/list (Report Issues)
> ---
> You received this message because you are subscribed to a topic in the Google Groups "web2py-users" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/web2py/QtP9re5WqqE/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to web2py+un...@googlegroups.com <mailto:web2py+un...@googlegroups.com>.

> For more options, visit https://groups.google.com/d/optout.


-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2

iQIcBAEBCAAGBQJVkXneAAoJEATsOw+FDrzIFloQAIYv09fk+uXW+kynEW4ZyxrP
HwjxVlEcDKUtFVLTpCCAI11nI4Mw/h+OSxfZ59wYVwEHGrz3bVrpwvB5auIwWvRR
1AH1h50TW6s/vkjF6/23NEYm2nLV6oPpKN/Evj4YDhAYFTb9VuWQoxEuW/Sjyf47
cUADobtBXKCAsXhXwGdZeO3bNRr+RNN5M3tmrOo4qxorK+gB3ExRLLS6NUCHHy+0
JHKTF4T2aP9GZoWDVElu+yrHwWsGf9qduf0Jj+YT8U4/5ac/f8fYCAPHQ8RmZa38
8GJKXh0Gb1c03aeu11ygPSFGxcuuZumQwpmcWFnH15qvs2UJtnEiy0xZOMEYe/cl
qcttu7GApclDTeYro6zHrmDKTFQHZ2FRmab/JsOBri705qGoB7i3O8QgnYb/57gf
5TamJsQD4rVkxKz2J7M66/vA+bYJ7O0HtgVyI2tuBnkVWjJ/p+FdvurPOF9pH9Le
AgzNaoh0OcuXQBgHcGORPIs07hLRppxQ0A1aiXYsu5asTW9q4CiZO/phfxdIX3da
8NpDgINhxjAEBJfnBmE3Qijoae9ysxW6z/BcaywjEWlXL7g9P3m3D0ZNKQ4pM+VP
u6PWvbbYrxZKpLEdyIsCr99EWleKUggvhagWtw5cZKFcF3auHppI2ZESTv5I919L
DMJPaZsu0diR4lOVIr6i
=0m8I
-----END PGP SIGNATURE-----

Massimo Di Pierro

unread,
Jun 29, 2015, 2:37:27 PM6/29/15
to web...@googlegroups.com, pa...@cancamusa.net
1. This should work

@auth.requires_login_or_token()
@auth.requires_membership('whatever')
def youtfunction():....

2. yes that is correct (except it is _token=, not token=). Just mind that the token is as good as a password and may be unsafe to send it via email. If if you need it, you can do it.
> To unsubscribe from this group and all its topics, send an email to web2py+unsubscribe@googlegroups.com <mailto:web2py+unsubscribe@googlegroups.com>.

> For more options, visit https://groups.google.com/d/optout.

Pablo Angulo

unread,
Jun 30, 2015, 6:06:36 PM6/30/15
to web...@googlegroups.com

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

This email was going to be a simple "Great!", but eventually I have many comments:

El 29/06/15 a las 20:37, Massimo Di Pierro escribió:

> 2. yes that is correct (except it is _token=, not token=). Just mind that the token is as good as a password and may be unsafe to send it via email. If if you need it, you can do it.

Is it less safe than sending auth.email_reset_password(), which anyone can ask? Actually, that's my number 1 use case. I want local admins to register users in bulk, then send each user an email that would automatically log her in, and send her right into a custom profile page. Depending on how unsafe it is, I may use for all the other emails, like for example: "click *here* to accept the new offer but click *here* to reject it", with no login.


More comments: If I understand the new code correctly:
1- the token is valid until admin explicitely takes action or it expires by date, but if I want for example one-use-only tokens, I have to write a custom "requires_login_or_token"
2- no token is created on user creation, so I have to create the tokens using for example a hook on db.auth_user._after_insert
3- a user can have many tokens, so I could create a token whenever I send her an email with auto-login. But is this sensible? What is the use case for having many tokens per user?


Actually, I've been using something related for some time. In some functions, say for example:

example.com/c/f?a=1&b=2

it is possible to share a link of the kind:

example.com/c/f?a=1&b=2&auth_code=SJAkKS_random_garbage_QWKSDW

If anyone gets the link with the correct auth_code, she can access the page. Otherwise, permissions depend on auth_membership:

>     auth.basic()                                                                
>     if productor.random_string==request.vars.auth_code:                         
>         pass                                                                    
>     elif not auth.login():                                                      
>         raise HTTP(403, T('You don't have permission. Please log in.'))
>     elif (auth.has_membership(role='admins_%s'%grupo) or                            
>           auth.has_membership(role='webadmins')):                               
>          pass                                                                   
>     else:                                                                       
>         session.flash = T('You don't have permission.')
>         redirect(URL(...)) 


I don't know if this pattern deserves its own generic implementation. Something like:
 auth.allow_login_by_keycode=1
that alters the behaviour of all decorators requires_login, requires_membership, requires, so that:
 - If request.vars._keycode is exactly hash(SALT+request.url), let them in
 - Otherwise, require login, membarship, etc
The admin would then decide when to share to link with the _keycode, if at all. There should be a function auth.get_keycode(url)...

Maybe we could use hash(SALT+request.url+'#'+user_id) to achieve both effects: if request.vars._keycode is exactly hash(SALT+request.url+'#'+user_id), you may access the url as if you were user_id. If the url is example.com/user/login?_next=/controller/function%3Fa%3D1&_keycode=XASXA_correct_keycode_ASKAJ, this automatically logs the user in, and redirects to example.com/controller/function?a=1, now logged as user_id. But there would be no need to use different decorators, and it also allows for other possibilities that I don't think I can do with requires_login_or_token.

I hope at least one of these comments make sense to you :-) !

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2

iQIcBAEBCAAGBQJVkxLiAAoJEATsOw+FDrzIhocP/0GUWq9q2bJXDJABXL3aJL0n
a1vTKnGWH00YiyhvXUyMBaIpkqhIj+kK423QVcJjqqEDQyVn2ctn9UhfnywoRUZ/
J18ECE1qZlW2+q44V7+v14s9RjSSNB47EVFfGz2xUv3xRhGYB25DZ/PdB0U2ZV+O
Q4j2/PqG6035IfHkCAX+jYgZLW0sv/KcFF28S0Dk2Ojd5SAWgVGcrz2ujq0jSoJO
SA7hqW1vYGmocAy0nkIM7pWiIoEPDo9UByUvEH/xsn1ArvEe4mQ8BVMry6w438yF
dnmGcPVWxbH5zriRwZEZJtwTvyVMMGLhVvpqkb4fTwSvKXfuZxxnuLCIJsRoaeb+
6o3sgDnzW7AYeds0haJz3MV0XAL9mtvls+aWrY9eAOxIvosT4Nu8e45FFzGrj0nl
maDTVTQJwMcGPqx+eK3FBIdcy0njYBwRvuWr5HJWx/aJf5DN30rUvWADDhH8jQ3d
GQs+3WlXrWrZWfM+yxLDJ/aUczkabZ73OaOb6aPfmzWGXGCEnY9nuYh1PXQIpuC/
ICW6+MZtQlu6Onl3irqt555hGh6SBSmPproFMp0yUiv0GuSvSY4uYP7KhQ9H96DE
NyKEGaWWm0Hhx0ZO8KntINYPbhpnCHIRBJVUpT2XZss6YgX0iMHEhw/GXRO4BryC
A96YHISuVuyCIVpJyg1h
=WSf2
-----END PGP SIGNATURE-----

Pablo Angulo

unread,
Jul 1, 2015, 3:14:20 AM7/1/15
to web...@googlegroups.com

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Just one more comment: I wrote hash(SALT+request.url+'#'+user_id) but meant hash(SUPER_SECRET_GLOBAL_PASSWORD+request.url+'#'+user_id)

El 01/07/15 a las 00:06, Pablo Angulo escribió:
> --
> Resources:
> - http://web2py.com
> - http://web2py.com/book (Documentation)
> - http://github.com/web2py/web2py (Source code)
> - https://code.google.com/p/web2py/issues/list (Report Issues)
> ---
> You received this message because you are subscribed to a topic in the Google Groups "web2py-users" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/web2py/QtP9re5WqqE/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to web2py+un...@googlegroups.com <mailto:web2py+un...@googlegroups.com>.

Niphlod

unread,
Jul 8, 2015, 4:16:31 PM7/8/15
to web...@googlegroups.com
summarizing, IMHO web2py should probably implement JWT tokens instead of this custom one to have it called properly "API tokens"

Derek

unread,
Jul 8, 2015, 6:25:54 PM7/8/15
to web...@googlegroups.com
The only difference between this and jwt (saying jwt tokens is like saying atm machine, it's redundant) is that jwt can be generated client side (provided the client knows the secret) and thus would be less secure than this.

Niphlod

unread,
Jul 9, 2015, 2:16:43 AM7/9/15
to web...@googlegroups.com
do you know what a jwt token is instead of just blindly bashing a solution?
"this" adds a table potentially growing towards infinite (to maintain), two queries, creates the session, for each and every request: all of which is unnecessary. 
It's web2py's proprietary, needs to be managed within a grid (so within the app), it's user-managed, hasn't integration with ANY other language, isn't extentable, doesn't have a refresh API, etc.

with jwt there's a clean API on how to let them expire, another to refresh them, how to store an additional payload, is extendable, has integration with multiple languages, it's documented, tested, engineered and pushed by people far more experienced in auth APIs. Reinventing the wheel is cool, until it's not.
You send a request for a token with username and password, you get a string back. Those are signed tokens, so unless you discover the secret, are unusable. 
They're as secure as signed urls.

Massimo Di Pierro

unread,
Jul 9, 2015, 8:53:17 AM7/9/15
to web...@googlegroups.com, nip...@gmail.com
I think we should support jwt. Niphlod. Can you provide an implementation?

Derek

unread,
Jul 9, 2015, 4:02:43 PM7/9/15
to web...@googlegroups.com
Yes, I did read up on it, and I am familiar with jwt. I do think it's more insecure than this.


On Wednesday, July 8, 2015 at 11:16:43 PM UTC-7, Niphlod wrote:
do you know what a jwt token is instead of just blindly bashing a solution?

Massimo Di Pierro

unread,
Jul 10, 2015, 1:45:30 AM7/10/15
to web...@googlegroups.com, sp1...@gmail.com
Can you elaborate. I like the current system (I wrote it ;-) but I was considering adding jwt. The question is, is there duplication of functionality? Should jwt replace the current token system? pros/cons?

Pablo Angulo

unread,
Jul 10, 2015, 4:43:27 AM7/10/15
to web...@googlegroups.com
Can you elaborate. I like the
      current system (I wrote it ;-) but I was considering adding jwt.
      The question is, is there duplication of functionality? Should jwt
      replace the current token system? pros/cons?

jwt can be used for much more stuff. A standard example: in a dropbox/seafile/etc clone, you set permissions for each file and folder, but then it's pretty simple to share links for downloading individual files.

I have written a few lines with my proposal, just to give you an idea.

In appconfig.ini:

 auth.jwt_secret='secret_password'


Then you can share a link for a page with:

def get_master_code():
    url
= auth.get_authorization_url(a='welcome', c='default', f='master_zone', vars=dict(message='Greetings'))
   
return dict(a = A('copy this link', _href=url))


users of that link will be automatically granted access to that a/c/f and with that vars.

@auth.requires_membership('masters')
 
def master_zone():
     
return dict(a='Hello, Master', b=request.vars.message)
 


With small changes, this can also be used to authenticate (if payload['_user']: ...)

Pablo Angulo

unread,
Jul 10, 2015, 5:49:00 AM7/10/15
to web...@googlegroups.com
Sorry, I see signing the email scrambled the patch:

diff --git a/applications/welcome/controllers/default.py b/applications/welcome/controllers/default.py
index c775603..e99ef0f 100644
--- a/applications/welcome/controllers/default.py
+++ b/applications/welcome/controllers/default.py
@@ -57,4 +57,10 @@ def call():
     """
     return service()
 
+...@auth.requires_membership('masters')
+def master_zone():
+    return dict(a='Hello, Master', b=request.vars.message)
 
+def get_master_code():
+    url = auth.get_authorization_url(a='welcome', c='default', f='master_zone', vars=dict(message='Greetings'))
+    return dict(a = A('copy this link', _href=url))
diff --git a/applications/welcome/languages/es.py b/applications/welcome/languages/es.py
index 7579cc3..54a57c6 100644
--- a/applications/welcome/languages/es.py
+++ b/applications/welcome/languages/es.py
@@ -55,6 +55,7 @@
 'Available Databases and Tables': 'Bases de datos y tablas disponibles',
 'Back': 'Atrás',
 'Buy this book': 'Compra este libro',
+"Buy web2py's book": "Buy web2py's book",
 'Cache': 'Caché',
 'cache': 'caché',
 'Cache Keys': 'Llaves de la Caché',
@@ -83,6 +84,7 @@
 'compile': 'compilar',
 'compiled application removed': 'aplicación compilada eliminada',
 'Components and Plugins': 'Componentes y Plugins',
+'Config.ini': 'Config.ini',
 'contains': 'contiene',
 'Controller': 'Controlador',
 'Controllers': 'Controladores',
@@ -119,6 +121,7 @@
 'Description': 'Descripción',
 'design': 'diseño',
 'DESIGN': 'DISEÑO',
+'Design': 'Design',
 'Design for': 'Diseño por',
 'detecting': 'detectando',
 'DISK': 'DISCO',
@@ -145,6 +148,7 @@
 'End of impersonation': 'Fin de suplantación',
 'enter a number between %(min)g and %(max)g': 'introduzca un número entre %(min)g y %(max)g',
 'enter a value': 'introduzca un valor',
+'Enter an integer between %(min)g and %(max)g': 'Enter an integer between %(min)g and %(max)g',
 'enter an integer between %(min)g and %(max)g': 'introduzca un entero entre %(min)g y %(max)g',
 'enter date and time as %(format)s': 'introduzca fecha y hora como %(format)s',
 'Error logs for "%(app)s"': 'Bitácora de errores en "%(app)s"',
@@ -177,6 +181,7 @@
 'Groups': 'Grupos',
 'Hello World': 'Hola Mundo',
 'help': 'ayuda',
+'Helping web2py': 'Helping web2py',
 'Home': 'Inicio',
 'How did you get here?': '¿Cómo llegaste aquí?',
 'htmledit': 'htmledit',
@@ -217,6 +222,7 @@
 'License for': 'Licencia para',
 'Live Chat': 'Chat en vivo',
 'loading...': 'cargando...',
+'Log In': 'Log In',
 'Logged in': 'Sesión iniciada',
 'Logged out': 'Sesión finalizada',
 'Login': 'Inicio de sesión',
@@ -256,6 +262,7 @@
 'not in': 'no en',
 'Object or table name': 'Nombre del objeto o tabla',
 'Old password': 'Contraseña vieja',
+'Online book': 'Online book',
 'Online examples': 'Ejemplos en línea',
 'Or': 'O',
 'or import from csv file': 'o importar desde archivo CSV',
@@ -319,6 +326,7 @@
 'Services': 'Servicios',
 'session expired': 'sesión expirada',
 'shell': 'terminal',
+'Sign Up': 'Sign Up',
 'site': 'sitio',
 'Size of cache:': 'Tamaño de la Caché:',
 'some files could not be removed': 'algunos archivos no pudieron ser removidos',
diff --git a/applications/welcome/models/db.py b/applications/welcome/models/db.py
index 606dc6f..3438621 100644
--- a/applications/welcome/models/db.py
+++ b/applications/welcome/models/db.py
@@ -8,6 +8,7 @@
 ## if SSL/HTTPS is properly configured and you want all HTTP requests to
 ## be redirected to HTTPS, uncomment the line below:
 # request.requires_https()
+import random
 
 ## app configuration made easy. Look inside private/appconfig.ini
 from gluon.contrib.appconfig import AppConfig
@@ -71,6 +72,8 @@ auth.settings.registration_requires_verification = False
 auth.settings.registration_requires_approval = False
 auth.settings.reset_password_requires_verification = True
 
+auth.settings.jwt_secret = myconf.take('auth.jwt_secret')
+
 #########################################################################
 ## Define your tables below (or better in another model file) for example
 ##
diff --git a/applications/welcome/private/appconfig.ini b/applications/welcome/private/appconfig.ini
index f45efbf..6db7544 100644
--- a/applications/welcome/private/appconfig.ini
+++ b/applications/welcome/private/appconfig.ini
@@ -8,7 +8,7 @@ pool_size = 1
 
 ; smtp address and credentials
 [smtp]
-server = smtp.gmail.com:587
+server = logging
 sender = y...@gmail.com
 login  = username:password
 
@@ -16,4 +16,7 @@ login  = username:password
 ; form styling
 [forms]
 formstyle = bootstrap3_inline
-separator =
\ No newline at end of file
+separator =
+
+[auth]
+jwt_secret = very_secret_even_random
diff --git a/gluon/tools.py b/gluon/tools.py
index 9d90b9a..da18795 100644
--- a/gluon/tools.py
+++ b/gluon/tools.py
@@ -1171,6 +1171,8 @@ class Auth(object):
         remember_me_form=True,
         allow_basic_login=False,
         allow_basic_login_only=False,
+        jwt_algorithm='HS512',
+        jwt_secret='',
         on_failed_authentication=lambda x: redirect(x),
         formstyle=None,
         label_separator=None,
@@ -3724,6 +3726,16 @@ class Auth(object):
             raise HTTP(403, 'ACCESS DENIED')
         return self.messages.access_denied
 
+    def get_authorization_url(self, a, c, f, vars):
+        keycode = self.get_authorization_code(a, c, f, vars)
+        return URL(a=a, c=c, f=f, vars=dict(_keycode=keycode))
+
+    def get_authorization_code(self, a, c, f, vars):
+        import jwt
+        payload = {'_application':a, '_controller':c, '_function':f}
+        payload.update(vars)
+        return jwt.encode(payload, self.settings.jwt_secret, algorithm=self.settings.jwt_algorithm)
+
     def requires(self, condition, requires_login=True, otherwise=None):
         """
         Decorator that prevents access to action if not logged in
@@ -3732,27 +3744,47 @@ class Auth(object):
         def decorator(action):
 
             def f(*a, **b):
+                r = current.request
 
                 basic_allowed, basic_accepted, user = self.basic()
                 user = user or self.user
-                if requires_login:
+
+                authorization_failed = False
+                if self.settings.jwt_secret and r.vars._keycode:
+                    import jwt
+                    try:
+                        encoded = r.vars._keycode
+                        payload = jwt.decode(encoded, self.settings.jwt_secret, algorithms=[self.settings.jwt_algorithm])
+                    except (jwt.DecodeError, jwt.InvalidTokenError):
+                        authorization_failed = True
+                    if not authorization_failed:
+                        acf = tuple(payload.pop(s) for s in ('_application', '_controller', '_function'))
+                        if acf != (r.application, r.controller, r.function):
+                            authorization_failed = True
+                        r.vars.clear()
+                        r.vars.update(payload)
+                        condition = True
+                elif requires_login:
                     if not user:
-                        if current.request.ajax:
-                            raise HTTP(401, self.messages.ajax_failed_authentication)
-                        elif not otherwise is None:
-                            if callable(otherwise):
-                                return otherwise()
-                            redirect(otherwise)
-                        elif self.settings.allow_basic_login_only or \
-                                basic_accepted or current.request.is_restful:
-                            raise HTTP(403, "Not authorized")
-                        else:
-                            next = self.here()
-                            current.session.flash = current.response.flash
-                            return call_or_redirect(
-                                self.settings.on_failed_authentication,
-                                self.settings.login_url +
-                                    '?_next=' + urllib.quote(next))
+                        authorization_failed = True
+
+                if authorization_failed:
+                    if r.ajax:
+                        raise HTTP(401, self.messages.ajax_failed_authentication)
+                    elif not otherwise is None:
+                        if callable(otherwise):
+                            return otherwise()
+                        redirect(otherwise)
+                    elif self.settings.allow_basic_login_only or \
+                            basic_accepted or r.is_restful:
+                        raise HTTP(403, "Not authorized")
+                    else:
+                        next = self.here()
+                        current.session.flash = current.response.flash
+                        return call_or_redirect(
+                            self.settings.on_failed_authentication,
+                            self.settings.login_url +
+                                '?_next=' + urllib.quote(next))
 
                 if callable(condition):
                     flag = condition()

jwt.patch

Derek

unread,
Jul 13, 2015, 3:47:27 PM7/13/15
to web...@googlegroups.com, sp1...@gmail.com
It's pretty much like client side cookies for sessions.
Reply all
Reply to author
Forward
0 new messages