Authentication and Authorization¶
Introduction to Security¶
Authentication is the mechanism whereby systems may securely identify their users. Eve supports several authentication schemes: Basic Authentication, Token Authentication, HMAC Authentication. OAuth2 integration is easily accomplished.
Authorization is the mechanism by which a system determines what level of
access a particular (authenticated) user should have access to resources
controlled by the system. In Eve, you can restrict access to all API endpoints,
or only some of them. You can protect some HTTP verbs while leaving others
open. For example, you can allow public read-only access while leaving item
creation and edition restricted to authorized users only. You can also allow
GET
access for certain requests and POST
access for others by checking
the method parameter. There is also support for role-based access control.
Security is one of those areas where customization is very important. This is
why you are provided with a handful of base authentication classes. They
implement the basic authentication mechanism and must be subclassed in order
to implement authorization logic. No matter which authentication scheme you
pick the only thing that you need to do in your subclass is override the
check_auth()
method.
Global Authentication¶
To enable authentication for your API just pass the custom auth class on
app instantiation. In our example we’re going to use the BasicAuth
base
class, which implements the Basic Authentication scheme:
from eve.auth import BasicAuth
class MyBasicAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource,
method):
return username == 'admin' and password == 'secret'
app = Eve(auth=MyBasicAuth)
app.run()
All your API endpoints are now secured, which means that a client will need to provide the correct credentials in order to consume the API:
$ curl -i http://example.com
HTTP/1.1 401 UNAUTHORIZED
Please provide proper credentials.
$ curl -H "Authorization: Basic YWRtaW46c2VjcmV0" -i http://example.com
HTTP/1.1 200 OK
By default access is restricted to all endpoints for all HTTP verbs (methods), effectively locking down the whole API.
But what if your authentication logic is more complex, and you only want to secure some endpoints or apply different logics depending on the endpoint being consumed? You could get away with just adding logic to your authentication class, maybe with something like this:
class MyBasicAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
if resource in ('zipcodes', 'countries'):
# 'zipcodes' and 'countries' are public
return True
else:
# all the other resources are secured
return username == 'admin' and password == 'secret'
If needed, this approach also allows to take the request method
into
consideration, for example to allow GET
requests for everyone while forcing
validation on edits (POST
, PUT
, PATCH
, DELETE
).
Endpoint-level Authentication¶
The one class to bind them all approach seen above is probably good for most use cases but as soon as authorization logic gets more complicated it could easily lead to complex and unmanageable code, something you don’t really want to have when dealing with security.
Wouldn’t it be nice if we could have specialized auth classes that we could freely apply to selected endpoints? This way the global level auth class, the one passed to the Eve constructor as seen above, would still be active on all endpoints except those where different authorization logic is needed. Alternatively, we could even choose to not provide a global auth class, effectively making all endpoints public, except the ones we want protected. With a system like this we could even choose to have some endpoints protected with, say, Basic Authentication while others are secured with Token, or HMAC Authentication!
Well, turns out this is actually possible by simply enabling the
resource-level authentication
setting when we are defining the API
domain.
DOMAIN = {
'people': {
'authentication': MySuperCoolAuth,
...
},
'invoices': ...
}
And that’s it. The people endpoint will now be using the MySuperCoolAuth
class for authentication, while the invoices
endpoint will be using the
general-purpose auth class if provided or else it will just be open to the
public.
There are other features and options that you can use to reduce complexity in your auth classes, especially (but not only) when using the global level authentication system. Lets review them.
Global Endpoint Security¶
You might want a public read-only API where only authorized users can write,
edit and delete. You can achieve that by using the PUBLIC_METHODS
and
PUBLIC_ITEM_METHODS
global settings. Add the following to
your settings.py:
PUBLIC_METHODS = ['GET']
PUBLIC_ITEM_METHODS = ['GET']
And run your API. POST, PATCH and DELETE are still restricted, while GET is
publicly available at all API endpoints. PUBLIC_METHODS
refers to resource
endpoints, like /people
, while PUBLIC_ITEM_METHODS
refers to individual
items like /people/id
.
Custom Endpoint Security¶
Suppose that you want to allow public read access to only certain resources. You do that by declaring public methods at resource level, while declaring the API domain:
DOMAIN = {
'people': {
'public_methods': ['GET'],
'public_item_methods': ['GET'],
},
}
Be aware that, when present, resource settings override global
settings. You can use this to your advantage. Suppose that you want to grant
read access to all endpoints with the only exception of /invoices
. You
first open read access for all endpoints:
PUBLIC_METHODS = ['GET']
PUBLIC_ITEM_METHODS = ['GET']
Then you protect the private endpoint:
DOMAIN = {
'invoices': {
'public_methods': [],
'public_item_methods': [],
}
}
Effectively making invoices a restricted resource.
Basic Authentication¶
The eve.auth.BasicAuth
class allows the implementation of Basic
Authentication (RFC2617). It should be subclassed in order to implement custom
authentication.
Basic Authentication with bcrypt¶
Encoding passwords with bcrypt is a great idea. It comes at the cost of performance, but that’s precisely the point, as slow encoding means very good resistance to brute-force attacks. For a faster (and less safe) alternative, see the SHA1/MAC snippet further below.
This script assumes that user accounts are stored in an accounts MongoDB collection, and that passwords are stored as bcrypt hashes. All API resources/methods will be secured unless they are made explicitly public.
Please note
You will need to install py-bcrypt for this to work.
# -*- coding: utf-8 -*-
"""
Auth-BCrypt
~~~~~~~~~~~
Securing an Eve-powered API with Basic Authentication (RFC2617).
You will need to install py-bcrypt: ``pip install py-bcrypt``
This snippet by Nicola Iarocci can be used freely for anything you like.
Consider it public domain.
"""
import bcrypt
from eve import Eve
from eve.auth import BasicAuth
from flask import current_app as app
class BCryptAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
# use Eve's own db driver; no additional connections/resources are used
accounts = app.data.driver.db['accounts']
account = accounts.find_one({'username': username})
return account and \
bcrypt.hashpw(password, account['password']) == account['password']
if __name__ == '__main__':
app = Eve(auth=BCryptAuth)
app.run()
Basic Authentication with SHA1/HMAC¶
This script assumes that user accounts are stored in an accounts MongoDB collection, and that passwords are stored as SHA1/HMAC hashes. All API resources/methods will be secured unless they are made explicitly public.
# -*- coding: utf-8 -*-
"""
Auth-SHA1/HMAC
~~~~~~~~~~~~~~
Securing an Eve-powered API with Basic Authentication (RFC2617).
Since we are using werkzeug we don't need any extra import (werkzeug being
one of Flask/Eve prerequisites).
This snippet by Nicola Iarocci can be used freely for anything you like.
Consider it public domain.
"""
from eve import Eve
from eve.auth import BasicAuth
from werkzeug.security import check_password_hash
from flask import current_app as app
class Sha1Auth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
# use Eve's own db driver; no additional connections/resources are used
accounts = app.data.driver.db['accounts']
account = accounts.find_one({'username': username})
return account and \
check_password_hash(account['password'], password)
if __name__ == '__main__':
app = Eve(auth=Sha1Auth)
app.run()
Token-Based Authentication¶
Token-based authentication can be considered a specialized version of Basic Authentication. The Authorization header tag will contain the auth token as the username, and no password.
This script assumes that user accounts are stored in an accounts MongoDB collection. All API resources/methods will be secured unless they are made explicitly public (by fiddling with some settings you can open one or more resources and/or methods to public access -see docs).
# -*- coding: utf-8 -*-
"""
Auth-Token
~~~~~~~~~~
Securing an Eve-powered API with Token based Authentication.
This snippet by Nicola Iarocci can be used freely for anything you like.
Consider it public domain.
"""
from eve import Eve
from eve.auth import TokenAuth
from flask import current_app as app
class TokenAuth(TokenAuth):
def check_auth(self, token, allowed_roles, resource, method):
"""For the purpose of this example the implementation is as simple as
possible. A 'real' token should probably contain a hash of the
username/password combo, which should then be validated against the account
data stored on the DB.
"""
# use Eve's own db driver; no additional connections/resources are used
accounts = app.data.driver.db['accounts']
return accounts.find_one({'token': token})
if __name__ == '__main__':
app = Eve(auth=TokenAuth)
app.run()
HMAC Authentication¶
The eve.auth.HMACAuth
class allows for custom, Amazon S3-like, HMAC (Hash
Message Authentication Code) authentication, which is basically a very secure
custom authentication scheme built around the Authorization header.
How HMAC Authentication Works¶
The server provides the client with a user id and a secret key through some out-of-band technique (e.g., the service sends the client an e-mail containing the user id and secret key). The client will use the supplied secret key to sign all requests.
When the client wants to send a request, he builds the complete request and then, using the secret key, computes a hash over the complete message body (and optionally some of the message headers if required)
Next, the client adds the computed hash and his userid to the message in the Authorization header:
Authorization: johndoe:uCMfSzkjue+HSDygYB5aEg==
and sends it to the service. The service retrieves the userid from the message header and searches the private key for that user in its own database. Next it computes the hash over the message body (and selected headers) using the key to generate its hash. If the hash the client sends matches the hash the server computes, then the server knows the message was sent by the real client and was not altered in any way.
Really the only tricky part is sharing a secret key with the user and keeping that secure. That is why some services allow for generation of shared keys with a limited life time so you can give the key to a third party to temporarily work on your behalf. This is also the reason why the secret key is generally provided through out-of-band channels (often a webpage or, as said above, an email or plain old paper).
The eve.auth.HMACAuth
class also support access roles.
HMAC Example¶
The snippet below can also be found in the examples/security folder of the Eve repository.
from eve import Eve
from eve.auth import HMACAuth
from flask import current_app as app
from hashlib import sha1
import hmac
class HMACAuth(HMACAuth):
def check_auth(self, userid, hmac_hash, headers, data, allowed_roles,
resource, method):
# use Eve's own db driver; no additional connections/resources are
# used
accounts = app.data.driver.db['accounts']
user = accounts.find_one({'userid': userid})
if user:
secret_key = user['secret_key']
# in this implementation we only hash request data, ignoring the
# headers.
return user and \
hmac.new(str(secret_key), str(data), sha1).hexdigest() == \
hmac_hash
if __name__ == '__main__':
app = Eve(auth=HMACAuth)
app.run()
Role Based Access Control¶
The code snippets above deliberately ignore the allowed_roles
parameter.
You can use this parameter to restrict access to authenticated users who also
have been assigned specific roles.
First, you would use the new ALLOWED_ROLES
and ALLOWED_ITEM_ROLES
global
settings (or the corresponding allowed_roles
and allowed_item_roles
resource settings).
ALLOWED_ROLES = ['admin']
Then your subclass would implement the authorization logic by making good use
of the aforementioned allowed_roles
parameter.
The snippet below assumes that user accounts are stored in an accounts MongoDB collection, that passwords are stored as SHA1/HMAC hashes and that user roles are stored in a ‘roles’ array. All API resources/methods will be secured unless they are made explicitly public.
# -*- coding: utf-8 -*-
"""
Auth-SHA1/HMAC-Roles
~~~~~~~~~~~~~~~~~~~~
Securing an Eve-powered API with Basic Authentication (RFC2617) and user
roles.
Since we are using werkzeug we don't need any extra import (werkzeug being
one of Flask/Eve prerequisites).
This snippet by Nicola Iarocci can be used freely for anything you like.
Consider it public domain.
"""
from eve import Eve
from eve.auth import BasicAuth
from werkzeug.security import check_password_hash
from flask import current_app as app
class RolesAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
# use Eve's own db driver; no additional connections/resources are used
accounts = app.data.driver.db['accounts']
lookup = {'username': username}
if allowed_roles:
# only retrieve a user if his roles match ``allowed_roles``
lookup['roles'] = {'$in': allowed_roles}
account = accounts.find_one(lookup)
return account and check_password_hash(account['password'], password)
if __name__ == '__main__':
app = Eve(auth=RolesAuth)
app.run()
User-Restricted Resource Access¶
When this feature is enabled, each stored document is associated with the account that created it. This allows the API to transparently serve only account-created documents on all kinds of requests: read, edit, delete and of course create. User authentication needs to be enabled for this to work properly.
At the global level this feature is enabled by setting AUTH_FIELD
and locally
(at the endpoint level) by setting auth_field
. These properties define the name
of the field used to store the id of the user who created the document. So for
example by setting AUTH_FIELD
to user_id
, you are effectively (and
transparently to the user) adding a user_id
field to every stored
document. This will then be used to retrieve/edit/delete documents stored by
the user.
But how do you set the auth_field
value? By invoking the
set_request_auth_value()
class method. Let us revise our
BCrypt-authentication example from above:
# -*- coding: utf-8 -*-
"""
Auth-BCrypt
~~~~~~~~~~~
Securing an Eve-powered API with Basic Authentication (RFC2617).
You will need to install py-bcrypt: ``pip install py-bcrypt``
This snippet by Nicola Iarocci can be used freely for anything you like.
Consider it public domain.
"""
import bcrypt
from eve import Eve
from eve.auth import BasicAuth
class BCryptAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
# use Eve's own db driver; no additional connections/resources are used
accounts = app.data.driver.db['accounts']
account = accounts.find_one({'username': username})
# set 'auth_field' value to the account's ObjectId
# (instead of _id, you might want to use ID_FIELD)
if account and '_id' in account:
self.set_request_auth_value(account['_id'])
return account and \
bcrypt.hashpw(password, account['password']) == account['password']
if __name__ == '__main__':
app = Eve(auth=BCryptAuth)
app.run()
Auth-driven Database Access¶
Custom authentication classes can also set the database that should be used when serving the active request.
Normally you either use a single database for the whole API or you configure
which database each endpoint consumes by setting mongo_prefix
to the
desired value (see Resource / Item Endpoints).
However, you might opt to select the target database based on the active token,
user or client. This is handy if your use-case includes user-dedicated database
instances. All you have to do is set invoke the set_mongo_prefix()
method
when authenticating the request.
A trivial example would be:
from eve.auth import BasicAuth
class MyBasicAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
if username == 'user1':
self.set_mongo_prefix('MONGO1')
elif username == 'user2':
self.set_mongo_prefix('MONGO2')
else:
# serve all other users from the default db.
self.set_mongo_prefix(None)
return username is not None and password == 'secret'
app = Eve(auth=MyBasicAuth)
app.run()
The above class will serve user1
with data coming from the database which
configuration settings are prefixed by MONGO1
in settings.py
. Same
happens with user2
and MONGO2
while all other users are served with
the default database.
Since values set by set_mongo_prefix()
have precedence over both default
and endpoint-level mongo_prefix
settings, what happens here is that the two
users will always be served from their reserved databases, no matter the
eventual database configuration for the endpoint.
OAuth2 Integration¶
Since you have total control over the Authorization process, integrating OAuth2 with Eve is easy. Make yourself comfortable with the topics illustrated in this page, then head over to Eve-OAuth2, an example project which leverages Flask-Sentinel to demonstrate how you can protect your API with OAuth2.
Please note
The snippets in this page can also be found in the examples/security folder of the Eve repository.