Skip to content

Commit

Permalink
Merge pull request #58 from RamanjaneyuluIdavalapati/master
Browse files Browse the repository at this point in the history
Authentication supported
  • Loading branch information
Ram Idavalapati authored Jul 19, 2018
2 parents bbb8e60 + a015ca9 commit 33c9e7b
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 8 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ deploy:
- LICENSE
- kwikapi/api.py
- kwikapi/__init__.py
name: kwikapi-0.3.6
tag_name: 0.3.6
name: kwikapi-0.3.7
tag_name: 0.3.7
on:
repo: deep-compute/kwikapi
- provider: pypi
Expand Down
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,113 @@ print(c.namespace.add(a=10, b=10))
print(c(version='v2', prtocol='pickle').namespace.add(a=10, b=10))
```

### Authentication
KwikAPI supports `Basic` and `Bearer` authentication.

**Basic authentication:**

Setting up authentication at server side

```python
import tornado.ioloop
import tornado.web

from kwikapi.tornado import RequestHandler
from kwikapi import API, Request, BasicServerAuthenticator

class Calc(object):
def add(self, req: Request, a: int, b: int) -> int:
if not req.auth.is_authenticated:
raise Exception("No auth") # Or your logic
return a + b

user_store = dict(johndoe=dict(password='password'))
auth = BasicServerAuthenticator(user_store=user_store)
api = API(auth=auth)
api.register(Calc(), 'v1')

def make_app():
return tornado.web.Application([
(r'^/api/.*', RequestHandler, dict(api=api)),
])
if __name__ == "__main__":
app = make_app()
app.listen(8818)
tornado.ioloop.IOLoop.current().start()
```

Using authentication at client side.
```python
from kwikapi import Client, BasicClientAuthenticator

auth = BasicClientAuthenticator(username='johndoe', password='password')

c = Client('http://localhost:8818/api/', version='v1', auth=auth, protocol='json')
print(c.add(a=10, b=10))
```

**Bearer Authentication**

Setting up authentication at server side

```python
import tornado.ioloop
import tornado.web

from kwikapi.tornado import RequestHandler
from kwikapi import API, BearerServerAuthenticator, Request

class Calc(object):
def add(self, req: Request, a: int, b: int) -> int:
if not req.auth.is_authenticated:
raise Exception("No auth")
return a + b

tokstore = dict(
key1 = dict(user='blah'),
key2 = dict(user='blah1'),
)
auth = BearerServerAuthenticator(token_store=tokstore)

api = API(auth=auth)
api.register(Calc(), 'v1')

def make_app():
return tornado.web.Application([
(r'^/api/.*', RequestHandler, dict(api=api)),
])
if __name__ == "__main__":
app = make_app()
app.listen(8818)
tornado.ioloop.IOLoop.current().start()
```

Using authentication at client side.
```python
from kwikapi import Client, BearerClientAuthenticator

auth = BearerClientAuthenticator('key2')

c = Client('http://localhost:8818/api/', version='v1', auth=auth, protocol='json')
print(c.add(a=10, b=10))
```

If you don't want to use KwikAPI client then you have to pass the authentication deatails through headers.

- Example for passing authentication details in Base Authentication.
```python
key = b'%s:%s' % (username, password)
key = base64.b64encode(key)
auth = b'Basic %s' % key
headers['Authorization'] = auth
```

- Example for passing authentication details in Bearer Authentication.
```python
auth = b'Bearer %s' % b'key'
headers['Authorization'] = auth
```

## Run test cases
```bash
$ python3 -m doctest -v README.md
Expand Down
11 changes: 11 additions & 0 deletions kwikapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,14 @@

from .client import Client, DNSCache
from . import exception

from .auth import BaseServerAuthenticator, \
AuthInfo, \
BasicServerAuthenticator, \
BearerServerAuthenticator, \
HMACServerAuthenticator, \
BaseClientAuthenticator, \
BasicClientAuthenticator, \
BearerClientAuthenticator, \
HMACClientAuthenticator, \
BaseAuthAPI
7 changes: 6 additions & 1 deletion kwikapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__(self):
self.protocol = None
self.metrics = {}
self._id = generate_random_string(length=5).decode('utf8')
self.auth = None

@property
def id(self):
Expand Down Expand Up @@ -187,9 +188,10 @@ class API(object):

def __init__(self, default_version=None, id='',
threadpool=None, threadpool_size=THREADPOOL_SIZE,
log=DUMMY_LOG):
auth=None, log=DUMMY_LOG):

self._api_funcs = {}
self._auth = auth
self.log = log.bind(api_id=id)
self._id = id
self.default_version = default_version
Expand Down Expand Up @@ -450,6 +452,9 @@ def _find_request_protocol(self, request):
return self.PROTOCOLS[protocol]

def handle_request(self, request):
if self.api._auth:
request.auth = self.api._auth.authenticate(request)

protocol = self._find_request_protocol(request)
request.protocol = protocol.get_name()
response = request.response
Expand Down
125 changes: 125 additions & 0 deletions kwikapi/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import abc
from abc import abstractmethod
import base64

from deeputil import xcode, AttrDict

from .api import Request

class BaseServerAuthenticator:
'''Helps in authenticating a request on the server'''
__metaclass__ = abc.ABCMeta

TYPE = 'base'

def _read_auth(self, req):
auth = req.headers.get('Authorization')
_type, info = auth.split(' ', 1)
if _type.lower() != self.TYPE:
raise Exception('Invalid auth type: %s' % _type) # FIXME: raise exc hierarchy

auth = AuthInfo(type=self.TYPE)
auth.header_info = info
return auth

def authenticate(self, req):
return self._read_auth(req)

class AuthInfo(AttrDict):
'''Represents authentication information (post auth)'''
__metaclass__ = abc.ABCMeta

def __init__(self, type=None):
self.type = type
self.is_authenticated = False

class BasicServerAuthenticator(BaseServerAuthenticator):
TYPE = 'basic'

def __init__(self, user_store=None):
self.user_store = user_store or {}

def authenticate(self, req):
auth = super().authenticate(req)
auth.username, auth.password = \
base64.b64decode(xcode(auth.header_info)).decode('utf-8').split(':')

auth_info = self.user_store.get(auth.username, None)
if not auth_info:
return auth

if auth_info.get('password', None) != auth.password:
return auth

auth.is_authenticated = True
auth.update(auth_info)

return auth

class BearerServerAuthenticator(BaseServerAuthenticator):
TYPE = 'bearer'

def __init__(self, token_store=None):
self.token_store = token_store or {}

def authenticate(self, req):
auth = super().authenticate(req)
auth.token = auth.header_info

auth_info = self.token_store.get(auth.token, None)
if auth_info:
auth.is_authenticated = True
auth.update(auth_info)

return auth

class HMACServerAuthenticator(BaseServerAuthenticator):
pass

class BaseClientAuthenticator:
'''Helps in signing a request with auth info'''
__metaclass__ = abc.ABCMeta

def __init__(self):
self.type = None

@abstractmethod
def sign(self, url, headers, body):
pass

class BasicClientAuthenticator(BaseClientAuthenticator):
def __init__(self, username, password):
self.username = xcode(username)
self.password = xcode(password)
self.encoded_key = b'%s:%s' % (self.username, self.password)
self.encoded_key = base64.b64encode(self.encoded_key)

def sign(self, url, headers, body):
headers['Authorization'] = b'Basic %s' % self.encoded_key

class BearerClientAuthenticator(BaseClientAuthenticator):
def __init__(self, bearer_token):
self.bearer_token = xcode(bearer_token)

def sign(self, url, headers, body):
headers['Authorization'] = b'Bearer %s' % self.bearer_token

class HMACClientAuthenticator(BaseClientAuthenticator):
pass

class BaseAuthAPI:
__metaclass__ = abc.ABCMeta

@abstractmethod
def login(self, req: Request, username: str, password: str) -> str:
pass

@abstractmethod
def logout(self, req: Request) -> None:
pass

@abstractmethod
def signup(self, req: Request,
username: str, password: str,
email: str) -> str:
pass
8 changes: 6 additions & 2 deletions kwikapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class Client:

def __init__(self, url, version=None, protocol=DEFAULT_PROTOCOL,
path=None, request='', timeout=None, dnscache=None,
headers=None, log=DUMMY_LOG):
headers=None, auth=None, log=DUMMY_LOG):

headers = headers or {}

Expand All @@ -59,6 +59,7 @@ def __init__(self, url, version=None, protocol=DEFAULT_PROTOCOL,
self._timeout = timeout
self._dnscache = dnscache
self._headers = CaseInsensitiveDict(headers)
self._auth = auth
self._log = log

if not self._dnscache:
Expand All @@ -69,7 +70,7 @@ def _get_state(self):
protocol=self._protocol, path=self._path,
request=self._request, timeout=self._timeout,
dnscache=self._dnscache, headers=self._headers,
log=self._log)
auth=self._auth, log=self._log)

def _copy(self, **kwargs):
_kwargs = self._get_state()
Expand Down Expand Up @@ -97,6 +98,9 @@ def _prepare_request(self, post_body, get_params=None):
url = '{}?{}'.format(url, urlencode(get_params))

url = self._dnscache.map_url(url)
if self._auth:
self._auth.sign(url, headers, post_body)

return url, post_body, headers

@staticmethod
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages

version = '0.3.6'
version = '0.3.7'
setup(
name="kwikapi",
version=version,
Expand All @@ -21,9 +21,9 @@
'requests==2.18.4',
],
extras_require={
'django': ['kwikapi-django==0.2.2'],
'django': ['kwikapi-django==0.2.3'],
'tornado': ['kwikapi-tornado==0.3'],
'all': ['kwikapi-django==0.2.2', 'kwikapi-tornado==0.3']
'all': ['kwikapi-django==0.2.3', 'kwikapi-tornado==0.3']
},
classifiers=[
'Environment :: Web Environment',
Expand Down

0 comments on commit 33c9e7b

Please sign in to comment.