So, the idea is to have something issuing a token that can be validated at every request without ever hitting the database.
No additional tables, horizontal scalability, good security, not really another login-contrib-method, well suited for APIs, etc.
I'm taking a long road here in order to explain some pretty specific choices done in the code.
Now, the "bad" part: Auth.login_user() is absolutely NOT built for speed. For a good reason in the username-password verification part (pbkdf2 comparison for password matching
protects the stored password from being guessed with a bruteforce in case they get stolen) but not so much for the update_groups() part.
In there, we get an additional query for each group the user is into (guess the usual nosql 2% of users can benefit from that but that's another story).
Long story short, up until now APIs were mainly built around basic auth....it's a special case into core (should go along with other login methods IMHO)
but even with basic authentication, and actions protected by a mere requires_login, which doesn't really require groups, we check for username, password (plus empty registration id, plus cycling through every other login_method appended), plus membership recursive query to get groups. At EVERY request.
Nice (not really), but let's get along for the sake of elevating RBAC as our model for authorization: group membership is really important!
It gets cached in session.auth.user_groups (and auth.user_groups) but .... groan! it's hardly leveraged by any piece of code. Maybe we - experienced developers - already know that auth.requires() is really - really, really, really - the only validator to use in addition to requires_login().
But I guess most users don't know that.
After all the heavy lifting of storing auth.user_groups (and loading session.auth.user_groups), requires_membership(), that is the de-facto recommended way to "protect" resources, doesn't use "cached" user_groups, but instead checks at runtime the membership (another nifty two-separate queries) for each request. Ultra-groan.
Now, this is pretty "ultrasecure" because you can remove the membership to a logged in user and at runtime have the permission effectively revoked.
Bak to the real word....let's be straight, there should be a specific API in auth to do this instead of promoting it as a default (maybe with a self-explanatory "please_kill_the_performances" argument).
Then we have the ultra-extra-ultra management of sessions, with all the fancy "renew/delete/whatever" callback. Let's not go there, too many groans :-P
Real-word scenario: every "modern" identity and authorization provider now issues a "something" that carries your identification and authorization specs, that are valid for a little while. This pattern scales horizontally really well, and one can adjust the "please_kill_the_performances" in favour of security just narrowing the expiration.
web2py's auth has been largely engineered for seldomly identifying the user, storing the identification in the session, and the not-so-seldomly checking
authorizations through group membership. In that vision, an "api tokens" auth is completely unsuitable to scale horizontally to protect something, if we implement it bashing the database at every request for identification and membership.
Enter the attached file: granted, needs testing, refinement and your approval, but it lends authentication and authorization storing what gets stored into
session inside a JWT token.
To avoid bashing the db, the real deal about this implemetation is that the token then gets loaded and injected into auth, in order to make auth decorators work.
No db, no login_user(), nothing at all.
Now, a taddle bit of wording on "security" (as users are always so worried about that).
The token is comprised of a header, a payload and a signature.
JWT specs are large enough to permit payload encryption, but IMHO it's a useless PITA (both to implement and to debug).
The provided module doesn't do encryption, it just signs (and verifies accordingly) the payload.
If the database needs to be left alone, the - obvious - attack vector of the implemetation (this of whatever will it come up) relies on the attacker knowing the secret.
Users - and developers - may get scared, and ask for the "jti" claim to be added and stored to check that the token used by the attacker has been effectively issued, etc etc etc. But this bashes the db. Maybe only a redis-backed storage could resist the pressure.
I'm planning to add subclassing (or callbacking) extensions to the module to make that (and other intricacies) possible (like storing additional claims in the payload), but I'd not recommend it by default.
Granted, if the attacker guesses the secret, he can craft a perfectly valid token for any user (with any claim in it), but the same goes for sessions stored in cookies (even if they are encrypted, as web2py uses a static secret by default).
Protection against bruteforcing should be done at a higher level that web2py, granted, but the implementation has a nice addition: users can salt the secret with their own function, maybe taking the hmac_key (a random uuid generated at "issuing token" time) as a starting point to generate the salt.
I hear you, "decoding the payload before verifying the signature is bad", but is a correct way to do that just from a theoretical standpoint. In this context, the worst thing happening is loading a json and not a pickle...so at most we loaded a Plain Old Python Dict.
No security issues there. There's also a max length defined for the header (to discourage DDOs) which some hardened webservers have already in place.
This makes the attack vector really smaller: an attacker needs to guess the secret AND the salt for every user. I don't think that we need to think about an additional layer of security (like encryption) to make the web2py's JWT api more secure.
I'm open to discussions and suggestions.
Please note that it's not bug free, nor tested (planned for this week).
It's just the first round of implemetation and, for all intents and purposes, a POC.
--
-- mail from:GoogleGroups "web2py-developers" mailing list
make speech: web2py-d...@googlegroups.com
unsubscribe: web2py-develop...@googlegroups.com
details : http://groups.google.com/group/web2py-developers
the project: http://code.google.com/p/web2py/
official : http://www.web2py.com/
---
You received this message because you are subscribed to the Google Groups "web2py-developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to web2py-develop...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
<web2py_jwt.py>
- 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
or a few lines down below
if self.algorithm not in ('HS256', 'HS384', 'HS512'): raise NotImplementedError('Algoritm %s not allowed' % algorithm)should do the trick :P
@auth.requires(lambda: 1 in auth.user_groups, requires_login=True) # if 1 is my admin group id...
Instead of :@auth.has_membership('admin')
--
--
--
The jti (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The jti claim can be used to prevent the JWT from being replayed. The jti value is a case-sensitive string. Use of this claim is OPTIONAL.
db.define_table('jwt_tokens', Field('token'), Field('user_id'), Field('inserted_on', 'datetime', default=request.now))
def myadditional_payload(payload):
res = db(db.jwt_tokens.user_id == payload['user']['id']).select(orderby=~db.jwt_tokens.inserted_on).first()
payload['jti'] = res.token
return payload
def mybefore_authorization(tokend):
res = db(
(db.jwt_tokens.user_id == tokend['user']['id']) &
(db.jwt_tokens.token == tokend['jti'])
).select().first()
if not res:
raise HTTP(400, u'Invalid JWT jti claim')
myjwt = Web2pyJwt('secret', auth,
additional_payload=additional_payload,
before_authorization=mybefore_authorization)
--
I agree with Simone that integration with other web2py functionality such as auth makes sense. web2py is a fullstack framework, one of its big advantages is that you don't have to lose time integrating a bunch of different modules because most of what you want already works out of the box. I don't want to have to do my own auth integration. I think it's easy enough to make your custom class inheriting from this one (we could just add a comment to the class saying which methods they need to override).