Authentication and authorization¶
Graceful offers very simple and extendable authentication and authorization mechanism. The main design principles for authentication and authorization in graceful are:
- Authentication (identifying users) and authorization (restricting access to the endpoint) are separate processes and because of that they should be declared separately.
- Available authentication schemes are gloabl and always the same for whole application.
- Different resources usually require different permissions so authorization is always defined on per-resource or per-method level.
- Authentication and authorization layers communicate only through request
context (the
req.context
attribute).
Thanks to these principles we are able to keep auth implementation very simple and also allow both mechanisms to be completely optional:
- You can replace the built-in authorization tools with your own custom middleware classes and hooks. You can also implement authorization layer inside of the resource modification methods (list/create/retrieve/etc.).
- If your use case is very simple and successful authentication
(user identification) allows for implicit access grant you can use only
the
authentication_required
decorator. - If you want to move whole authentication layer outside of your application code (e.g. using specialized reverse proxy) you can easily do that. The only thing you need to do is to create some middleware that will properly modify your request context dictionary to include proper user object.
Authentication - identifying the users¶
In order to define authentication for your application you need to instantiate one or more of the built in authentication middleware classes and configure falcon application to use them. For example:
api = application = falcon.API(middleware=[
authentication.XForwardedFor(),
authentication.Anonymous(),
])
If request made the by the user meets all the requirements that are specific to
any authentication flow, the generated/retrieved user object will be included
in request context under req.context['user']
key. If this context variable
exists it is a clear sign that request was succesfully authenticated.
If you use multiple different middleware classes only the first middleware that succeeded to identify the user will be resolved. This allows for having fallback authentication mechanism like anonymous users or users identified by remote address.
User objects and working with user storages¶
Most of authentication middleware classes provided in graceful require
user_storage
initializations argument. This is the object
that abstracts access to the authentication database. It should implement
at least the get_user()
method:
from graceful.authentication import BaseUserStorage
class CustomUserStorage(BaseUserStorage):
def get_user(
self, identified_with, identifier,
req, resp, resource, uri_kwargs
):
...
Accepted get_user()
method arguments are:
- identified_with (object): instance of the authentication middleware
that provided the
identifier
value. It allows to distinguish different types of user credentials. - identifier (object): object that identifies the user. It is specific for every authentication middleware implementation. For some middlewares it can be a raw string value (e.g. token or API key).
- req (falcon.Request): the request object.
- resp (falcon.Response): the response object. resource (object): the resource object.
- uri_kwargs (dict): keyword arguments from the URI template.
If user entry exists in the storage (user can be identified) the method should
return user object. This object usually is just a simple Python dictionary.
This object will be later included in the request context as
req.context['user']
variable. If user cannot be found in the storage
it means that his identifier is either fake or invalid. In such case this
method should always return None
.
Note
Note that at this stage you should not verify any user permissions. If you can identify user but it is unpriviledged client you should still return the user object. Actual permission checking is a responsibility of the authorization layer. You should inlcude all user metadata that will be later required in the authorization process.
Graceful inlcudes a few useful concrete user storage implementations:
KeyValueUserStorage
: simple implementation of user storage using any key-value database client as a storage backend.DummyUserStorage
: a dummy user storage that will always return the configured default user. It is useful only for testing purposes.IPRangeWhitelistStorage
: user storage with IP range whitelist intended to be used exclusively with theXForwardedFor
authentication middleware.
Implictit authentication without user storages¶
Some built-in authentication implementations for graceful do not require any user storage to be defined in order to work. These authentication schemes are provided in form of following middlewares:
authentication.XForwardedFor
: theuser_storage
argument is completely optional.authentication.Anonymous
: does not supportuser_storage
argument at all.
If XForwardedFor
is used without any storage it will sucessfully
identify every request. The resulting request object will be syntetic user
dictionary in following form:
{
'identified_with': <authenticator>,
'identifier': <user-address>
}
Where <authenticator>
is the authentication middleware instance (here
defaults to XForwardedFor
) and the indentity
will be
client’s address. Client address is either value of X-Forwarded-For
header
or remote address taken directly from WSGI enviroment dictionary (only if
middleware is configured with remote_address_fallback=True
).
In case of Anonymous
the resulting user context variable will be always
the same as the value of middleware’s user
initialization argument.
Both XForwardedFor
(without user storage) and Anonymous
are
intended to be used only as authentication fallbacks for applications that
expect req.context['user']
variable to be always available. This can be
useful for applications that identify every user to track and throttle API
usage on endpoints that do not require any authorization.
Custom authentication middleware¶
The easiest way to implement custom authentication middleware is by subclassing
the BaseAuthenticationMiddleware
. The only method you need to implement
is identify()
. It has access to following arguments:
identify(self, req, resp, resource, uri_kwargs):
- req (falcon.Request): falcon request object. You can read headers and get arguments from it.
- resp (falcon.Response): falcon response object. Usually not accessed during authentication.
- resource (object): resource object that request is routed to. May be useful if you want to provide dynamic realms.
- uri_kwags (dict): dictionary of keyword arguments from URI template.
Aditionally you can control further the behaviour of authentication middleware using following class attributes:
only_with_storage
: if it is set to True, it will be impossible to initialize the middleware withoutuser_storage
argument.challenge
: returns the challenge string that will be inlcuded inWWW-Authenticate
header on unauthorized request responses. This has effect only in resources protected withauthentication_required
.
Authorization - restricting access to the endpoint¶
The recommended way to implement authorization in graceful is through falcon hooks that can be applied to whole resources and HTTP method handlers:
import falcon
from graceful.resources.generic import ListAPI
falcon.before(my_authorization_hook)
class MyListResource(ListAPI):
...
@falcon.before(my_other_authorization_hook)
def on_post(self, *args, **kwargs)
return super().on_post()
Authorization hooks depend solely on user context stored under
req.context['user']
. The usual authorization hook implementation does two
things:
- Check if the
'user'
variable is available inreq.context
dictionary. If it isn’t then raise thefalcon.HTTPForbidden
exception. - Verify user object content (e.g. check his group) and raise the
falcon.HTTPForbidden
exception if does not meet specific requirements.
Example of customizable authorization hook implementation that requires specific user group to be assigned could be as follows:
import falcon
def group_required(user_group):
@falcon.before
def authorization_hook(req, resp, resource, uri_kwargs)
try:
user = req.context['user']
except KeyError:
raise falcon.HTTPForbidden(
"Forbidden",
"Could not identify the user!"
)
if user_group not in user.get('groups', set()):
raise falcon.HTTPForbidden(
"Forbidden",
"'{}' group required!".format(user_group)
)
Depending on your application design and complexity you will need different
authorization handling. The way how you grant/deny access also depends highly
on the structure of your user objects and the preferred user storage.
This is why graceful provides only one basic authorization utility - the
authentication_required
decorator.
The authentication_required
decorator ensures that request successfully
passed authentication. If none of the authentication middlewares succeeded
to identify the user it will raise falcon.HTTPUnauthorized
exception and include list of available authentication challenges in the
WWW-Authenticate
response header. If you use this decorator you don’t need
to check for req.context['user']
existence in your custom authorization
hooks (still, it is a good practice to do so).
Example usage is:
from graceful import authorization
from graceful.resources.generic import ListAPI
from myapp.auth import group_required
@authentication_required
@group_required("admin")
class MyListResource(ListAPI):
...
@falcon.before(my_other_authorization_hook)
def on_post(self, *args, **kwargs)
return super().on_post()
Heterogenous authentication¶
Graceful does not allow you to specify unique per-resource or per-method authentication schemes. This allows for easier implementation but may not cover every use case possible.
If you need to restrict some authentication methods to specific resources (e.g. some custom auth only for internal use) the best way is to handle this through separate application deployments.
Practical example – authentication with redis backend¶
Let’s assume we want to build simple REST API application supporting two authentication schemes:
Token
access authentication withAuthorization: Token
HTTP headerBasic
access authentication withAuthorization: Basic
HTTP header as specified by RFC 7617.
As a user database we will use KeyValueUserStorage
storage class which is
compatible with any key-value database client that provides two simple methods:
set(key, value)
: set key value in the storage. Both key and value should be strings.get(key)
: get key value from the storage. Both key and return value should be string.
First step is to create a key-value store client user storage intance that
will be used by both authentication middlewares. With redis and
KeyValueUserStorage
this is very simple:
from redis import StrictRedis as Redis
from graceful.authentication import KeyValueUserStorage
auth_storage = KeyValueUserStorage(Redis())
This storage can be used by many different authentication middlewares at the same time. It will properly prefix every Redis key with middleware name to make sure different types of user entries do not collide with each other.
The only problem is that default implementation of
KeyValueUserStorage.hash_identifier(identified_with, identifier)
method expects
that identifier
argument is a single string argument. The Basic
authentication middleware generates identifiers in form of
(username, password)
two-tuples. Fortunately you don’t need to use
subclassing in order to override this method behavior. The
hash_identifier
method is a single-dispatch generic function so you
can easily create custom handlers for specific authentication middleware types.
We definitely don’t want to store user passwords in plain text. Let’s register
simple hash_identifier
handler for Basic
access authentication that
will properly prepare password hash using SHA1 algorithm:
from hashlib import sha1
from graceful.authentication import Basic
@auth_storage.hash_identifier.register(Basic)
def _(identified_with, identifier):
return ":".join((
identifier[0],
hashlib.sha1(identifier[1].encode()).hexdigest()
))
Default hash_identifier
leaves single-string identifiers untouched so it
may be a good idea to hash token identifiers in similar fashion too:
@auth_storage.hash_identifier.register(Token)
def _(identified_with, identifier):
return hashlib.sha1(identifier[1].encode()).hexdigest()
Note
Really secure password verification mechanism would require proper time-consuming hashing algorithm that would prevent application from brute-force and timing attacks. Anyway, for real end-user applications you would probably use a session cookie for authentication rather than basic access authentication. For such case simple SHA1 hashing may not be the best solution. Still, basic access authentication is a simple alternative to custom authentication headers and/or GET parameters when communicating in server-to-server fashion over the secure channel.
Our authentication setup is almost finished. The last things to do is to
initialize authentication middlewares and setup a very basic authorization
to API resources. Following is the code for a very small application that
protects its resources with Token
and Basic
authentication
middlewares:
import hashlib
from redis import StrictRedis as Redis
import falcon
from graceful.resources.generic import Resource
from graceful.authentication import KeyValueUserStorage, Token, Basic
from graceful.authorization import authentication_required
@authentication_required
class Me(Resource, with_context=True):
def retrieve(self, params, meta, context):
return context.get('user')
auth_storage = KeyValueUserStorage(Redis())
@auth_storage.hash_identifier.register(Basic)
def _(identified_with, identifier):
return ":".join((
identifier[0],
hashlib.sha1(identifier[1].encode()).hexdigest()
))
@auth_storage.hash_identifier.register(Token)
def _(identified_with, identifier):
return hashlib.sha1(identifier[1].encode()).hexdigest()
api = application = falcon.API(
middleware=[
Token(auth_storage),
Basic(auth_storage),
]
)
api.add_route('/me/', Me())
Now you can easily create new user entries using Pyhton console:
>>> from auth_app import auth_storage, Token, Basic
>>> auth_storage.register(Token(auth_storage), 'mytoken', {"user": "me with token"})
>>> auth_storage.register(Basic(auth_storage), ['myusername', 'mysecretpassword'], {"user": "me with password"})
... check if they are successfully saved in Redis:
$ redis-cli keys '*'
1) "users:Token:95cb0bfd2977c761298d9624e4b4d4c72a39974a"
2) "users:Basic:myusername:08cd923367890009657eab812753379bdb321eeb"
... and verify authentication using HTTP client (here with httpie
):
$ http localhost:8000/me
HTTP/1.1 401 Unauthorized
Connection: close
Date: Thu, 23 Mar 2017 16:09:55 GMT
Server: gunicorn/19.6.0
content-length: 91
content-type: application/json
vary: Accept
www-authenticate: Token, Basic realm=api
{
"description": "This resource requires authentication",
"title": "Unauthorized"
}
$ http localhost:8000/me --auth myusername:mysecretpassword
HTTP/1.1 200 OK
Connection: close
Date: Thu, 23 Mar 2017 16:08:53 GMT
Server: gunicorn/19.6.0
content-length: 76
content-type: application/json
{
"content": {
"user": "me with password"
},
"meta": {
"params": {
"indent": 0
}
}
}
$ http localhost:8000/me 'Authorization:Token mytoken'
HTTP/1.1 200 OK
Connection: close
Date: Thu, 23 Mar 2017 16:09:39 GMT
Server: gunicorn/19.6.0
content-length: 73
content-type: application/json
{
"content": {
"user": "me with token"
},
"meta": {
"params": {
"indent": 0
}
}
}