Commit f5d74f8a authored by qhoangxuan's avatar qhoangxuan

move oauth2 store to postgres, simplify oauth2 rest api

parent dc1f388f
# Copyright (c) 2017, Quang-Nhat HOANG-XUAN
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Store adapters to read/write data to from/to PostgresSQL. """
import datetime, time
import zoe_lib.state
from zoe_lib.config import get_conf
from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore
from oauth2.datatype import AccessToken, AuthorizationCode, Client
from oauth2.error import AccessTokenNotFound, AuthCodeNotFound, ClientNotFoundError
class AccessTokenStore(AccessTokenStore):
def fetch_by_refresh_token(self, refresh_token):
sql = zoe_lib.state.SQLManager(get_conf())
data = sql.fetch_by_refresh_token(refresh_token)
if data is None:
raise AccessTokenNotFound
return AccessToken(client_id=data["client_id"],
grant_type=data["grant_type"],
token=data["token"],
data=data["data"],
expires_at=data["expires_at"].timestamp(),
refresh_token=data["refresh_token"],
refresh_expires_at=data["refresh_token_expires_at"].timestamp(),
scopes=data["scopes"])
def delete_refresh_token(self, refresh_token):
"""
Deletes (invalidates) an old refresh token after use
:param refresh_token: The refresh token.
"""
sql = zoe_lib.state.SQLManager(get_conf())
res = sql.delete_refresh_token(refresh_token)
return res
def get_client_id_by_access_token(self, access_token):
sql = zoe_lib.state.SQLManager(get_conf())
data = sql.get_client_id_by_access_token(access_token)
return data
def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
sql = zoe_lib.state.SQLManager(get_conf())
data = sql.fetch_existing_token_of_user(client_id, grant_type, user_id)
if data is None:
raise AccessTokenNotFound
return AccessToken(client_id=data["client_id"],
grant_type=data["grant_type"],
token=data["token"],
data=data["data"],
expires_at=data["expires_at"].timestamp(),
refresh_token=data["refresh_token"],
refresh_expires_at=data["refresh_token_expires_at"].timestamp(),
scopes=data["scopes"],
user_id=data["user_id"])
def save_token(self, access_token):
sql = zoe_lib.state.SQLManager(get_conf())
sql.save_token(access_token.client_id,
access_token.grant_type,
access_token.token,
access_token.data,
access_token.expires_at,
access_token.refresh_token,
access_token.refresh_expires_at,
access_token.scopes,
access_token.user_id)
return True
class ClientStore(ClientStore):
def save_client(self, identifier, secret, role, redirect_uris, authorized_grants, authorized_response_types):
sql = zoe_lib.state.SQLManager(get_conf())
sql.save_client(identifier,
secret,
role,
redirect_uris,
authorized_grants,
authorized_response_types)
return True
def fetch_by_client_id(self, client_id):
sql = zoe_lib.state.SQLManager(get_conf())
client_data = sql.fetch_by_client_id(client_id)
client_data_grants= client_data["authorized_grants"].split(':')
if client_data is None:
raise ClientNotFoundError
return Client(identifier=client_data["identifier"],
secret=client_data["secret"],
redirect_uris=client_data["redirect_uris"],
authorized_grants=client_data_grants,
authorized_response_types=client_data["authorized_response_types"])
def get_role_by_client_id(self, client_id):
sql = zoe_lib.state.SQLManager(get_conf())
client_data = sql.fetch_by_client_id(client_id)
if client_data is None:
raise ClientNotFoundError
return client_data["role"]
# Copyright (c) 2016, Quang-Nhat HOANG-XUAN
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Token generator for oauth2."""
import base64
import hashlib
import os
import uuid
class TokenGenerator(object):
"""
Base class of every token generator.
"""
def __init__(self):
"""
Create a new instance of a token generator.
"""
self.expires_in = {}
self.refresh_expires_in = 3600
def create_access_token_data(self, grant_type):
"""
Create data needed by an access token.
:param grant_type:
:type grant_type: str
:return: A ``dict`` containing he ``access_token`` and the
``token_type``. If the value of ``TokenGenerator.expires_in``
is larger than 0, a ``refresh_token`` will be generated too.
:rtype: dict
"""
if grant_type == 'password':
self.expires_in['password'] = 36000
result = {"access_token": self.generate(), "token_type": "Bearer"}
if self.expires_in.get(grant_type, 0) > 0:
result["refresh_token"] = self.generate()
result["expires_in"] = self.expires_in[grant_type]
return result
def generate(self):
"""
Implemented by generators extending this base class.
:raises NotImplementedError:
"""
raise NotImplementedError
class URandomTokenGenerator(TokenGenerator):
"""
Create a token using ``os.urandom()``.
"""
def __init__(self, length=40):
self.token_length = length
TokenGenerator.__init__(self)
def generate(self):
"""
:return: A new token
:rtype: str
"""
random_data = os.urandom(100)
hash_gen = hashlib.new("sha512")
hash_gen.update(random_data)
return hash_gen.hexdigest()[:self.token_length]
class Uuid4(TokenGenerator):
"""
Generate a token using uuid4.
"""
def generate(self):
"""
:return: A new token
:rtype: str
"""
return str(uuid.uuid4())
......@@ -78,7 +78,26 @@ def create_tables(cur):
ip_address CIDR NULL DEFAULT NULL,
essential BOOLEAN NOT NULL DEFAULT FALSE
)''')
#Create oauth_client and oauth_token tables for oAuth2
cur.execute('''CREATE TABLE oauth_client (
identifier TEXT PRIMARY KEY,
secret TEXT,
role TEXT,
redirect_uris TEXT,
authorized_grants TEXT,
authorized_response_types TEXT
)''')
cur.execute('''CREATE TABLE oauth_token (
client_id TEXT PRIMARY KEY,
grant_type TEXT,
token TEXT,
data TEXT,
expires_at TIMESTAMP,
refresh_token TEXT,
refresh_token_expires_at TIMESTAMP,
scopes TEXT,
user_id TEXT
)''')
def init():
"""DB init entrypoint."""
......
......@@ -13,24 +13,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""The Execution API endpoints."""
"""The oAuth2 API endpoints."""
from tornado.web import RequestHandler
import tornado.escape
import logging
import zoe_lib.config as config
from zoe_api.rest_api.utils import catch_exceptions, get_auth
from zoe_api.rest_api.oauth_utils import auth_controller, mongo, get_username_password
import zoe_api.exceptions
from zoe_api.api_endpoint import APIEndpoint # pylint: disable=unused-import
import oauth2.grant
import json
import requests
import pymongo
import psycopg2
from zoe_api.rest_api.utils import catch_exceptions, get_auth
from zoe_api.rest_api.oauth_utils import auth_controller, client_store, token_store
from zoe_api.api_endpoint import APIEndpoint
log = logging.getLogger(__name__)
......@@ -38,26 +37,16 @@ log = logging.getLogger(__name__)
Example of using:
-Two kind of request token:
(1) with an access token and a refresh token
(2) with only an access token
*To request a new token of type:
(1):
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"client_id": "admin", "client_secret": "admin", "grant_type": "password", "username": "admin", "password": "admin", "scope": ""}'
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"grant_type": "password"}'
Output: {"token_type": "Bearer", "access_token": "3ddbe9ba-6a21-4e4d-993b-70556390c5d3", "refresh_token": "9bab190f-e211-42aa-917e-20ce987e355e", "expires_in": 36000}
(2):
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"client_id": "admin", "client_secret": "admin", "grant_type": "client_credentials", "scope": ""}'
Output: {"token_type": "Bearer", "access_token": "e6ab7c66-777b-4b64-91e0-2f501d28fe6e", "expires_in": 3600}
*To refresh a token, only apply for type-1 request token
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"client_id": "admin", "client_secret": "admin", "grant_type": "refresh_token", "refresh_token": "9bab190f-e211-42aa-917e-20ce987e355e", "username": "admin", "password": "admin", "scope": ""}'
*To refresh a token
Input: curl -u 'admin:admin' http://localhost:5001/api/0.6/oauth/token -X POST -H 'Content-Type: application/json' -d '{"grant_type": "refresh_token", "refresh_token": "9bab190f-e211-42aa-917e-20ce987e355e"}'
Output: {"token_type": "Bearer", "access_token": "378f8d5f-2eb5-4181-b632-ad23c4534d32", "expires_in": 36000}
*To revoke a token, apply for two types of token requested
curl -i --verbose -u 'admin:admin' -X DELETE http://localhost:5001/api/0.6/oauth/revoke/e6ab7c66-777b-4b64-91e0-2f501d28fe6e
*To revoke a token, the passed token could be the access token or refresh token
curl -u 'admin:admin' -X DELETE http://localhost:5001/api/0.6/oauth/revoke/378f8d5f-2eb5-4181-b632-ad23c4534d32
*To authenticate with other rest api services, using a header with: "Authorization: Bearer access_token"
curl -H 'Authorization: Bearer 378f8d5f-2eb5-4181-b632-ad23c4534d32' http://localhost:5001/api/0.6/execution
......@@ -71,34 +60,35 @@ class OAuthGetAPI(RequestHandler):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
self.auth_controller = auth_controller
self.mongo = mongo
self.client_store = client_store
@catch_exceptions
def post(self):
"""REQUEST/REFRESH token"""
username, password = get_username_password(self)
uid, role = get_auth(self)
grant_type = oauth2.grant.RefreshToken.grant_type + ':' + oauth2.grant.ResourceOwnerGrant.grant_type
try:
self.mongo['db']['oauth_clients'].insert(
{'identifier': username,
'secret': password,
'redirect_uris': [],
'authorized_grants': [ oauth2.grant.RefreshToken.grant_type,
oauth2.grant.ResourceOwnerGrant.grant_type,
oauth2.grant.ClientCredentialsGrant.grant_type]})
except pymongo.errors.DuplicateKeyError as e:
log.warn("Already had this user in db, skip adding...")
self.client_store.save_client(uid, '', role, '', grant_type, '')
except psycopg2.IntegrityError as e:
log.warn('User is already had')
response = self._dispatch_request()
response = self._dispatch_request(uid)
self._map_response(response)
def _dispatch_request(self):
def _dispatch_request(self, uid):
request = self.request
request.post_param = lambda key: json.loads(request.body.decode())[key]
params = json.loads(request.body.decode())
params['password'] = ''
params['username'] = ''
params['client_secret'] = ''
params['scope'] = ''
params['client_id'] = uid
request.post_param = lambda key: params[key]
return self.auth_controller.dispatch(request, environ={})
def _map_response(self, response):
for name, value in list(response.headers.items()):
......@@ -119,17 +109,17 @@ class OAuthRevokeAPI(RequestHandler):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
self.auth_controller = auth_controller
self.token_store = token_store
@catch_exceptions
def delete(self, token):
"""DELETE token (logout)"""
uid, role = get_auth(self)
key = 'oauth2_{}'.format(token)
res = self.auth_controller.access_token_store.rs.delete(key)
res = self.token_store.delete_refresh_token(token)
if res == 0:
raise zoe_api.exceptions.ZoeRestAPIException('No token found in database')
ret = {'res': 'Revoked token.'}
ret = {'ret' :'No token found in database.'}
else:
ret = {'res': 'Revoked token.'}
self.write(ret)
# Copyright (c) 2016, Quang-Nhat HOANG-XUAN
# Copyright (c) 2017, Quang-Nhat HOANG-XUAN
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
......@@ -13,30 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility functions needed by the Zoe REST API."""
"""Authentication controller for oauth2."""
import base64
import logging
from zoe_lib.config import get_conf
from zoe_api.exceptions import ZoeRestAPIException
import tornado.web
import oauth2
import oauth2.tokengenerator
import oauth2.web
import oauth2.grant
import oauth2.store.redisdb
import oauth2.store.mongodb
from zoe_api.auth.oauth2.postgresql import AccessTokenStore, ClientStore
from zoe_api.auth.oauth2.tokengenerator import Uuid4
import time
import json
import fakeredis
import mongomock
import hashlib
import os
import uuid
log = logging.getLogger(__name__)
......@@ -44,101 +31,8 @@ class OAuthSiteAdapter(oauth2.web.ResourceOwnerGrantSiteAdapter):
def authenticate(self, request, environ, scopes, client):
return {}
class TokenGenerator(object):
"""
Base class of every token generator.
"""
def __init__(self):
"""
Create a new instance of a token generator.
"""
self.expires_in = {}
self.refresh_expires_in = 0
def create_access_token_data(self, grant_type):
"""
Create data needed by an access token.
:param grant_type:
:type grant_type: str
:return: A ``dict`` containing he ``access_token`` and the
``token_type``. If the value of ``TokenGenerator.expires_in``
is larger than 0, a ``refresh_token`` will be generated too.
:rtype: dict
"""
if grant_type == 'password':
self.expires_in['password'] = 36000
result = {"access_token": self.generate(), "token_type": "Bearer"}
if self.expires_in.get(grant_type, 0) > 0:
result["refresh_token"] = self.generate()
result["expires_in"] = self.expires_in[grant_type]
return result
def generate(self):
"""
Implemented by generators extending this base class.
:raises NotImplementedError:
"""
raise NotImplementedError
class URandomTokenGenerator(TokenGenerator):
"""
Create a token using ``os.urandom()``.
"""
def __init__(self, length=40):
self.token_length = length
TokenGenerator.__init__(self)
def generate(self):
"""
:return: A new token
:rtype: str
"""
random_data = os.urandom(100)
hash_gen = hashlib.new("sha512")
hash_gen.update(random_data)
return hash_gen.hexdigest()[:self.token_length]
class Uuid4(TokenGenerator):
"""
Generate a token using uuid4.
"""
def generate(self):
"""
:return: A new token
:rtype: str
"""
return str(uuid.uuid4())
def get_username_password(handler: tornado.web.RequestHandler):
"""Get username, password"""
auth_header = handler.request.headers.get('Authorization')
if auth_header is None or not (auth_header.startswith('Basic ') or auth_header.startswith('Bearer ')):
raise ZoeRestAPIException('missing or wrong authentication information', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
auth_decoded = base64.decodebytes(bytes(auth_header[6:], 'ascii')).decode('utf-8')
username, password = auth_decoded.split(':', 2)
return username, password
mongo = mongomock.MongoClient()
mongo['db']['oauth_clients'].ensure_index("identifier", unique=True)
client_store = oauth2.store.mongodb.ClientStore(mongo['db']['oauth_clients'])
token_store = oauth2.store.redisdb.TokenStore(rs=fakeredis.FakeStrictRedis())
client_store = ClientStore()
token_store = AccessTokenStore()
token_generator = Uuid4()
token_generator.expires_in[oauth2.grant.ClientCredentialsGrant.grant_type] = 3600
......@@ -152,7 +46,7 @@ auth_controller = oauth2.Provider(
site_adapter = OAuthSiteAdapter()
auth_controller.token_path = '/api/0.6/oauth/token'
auth_controller.token_path = '/api/0.7/oauth/token'
auth_controller.add_grant(oauth2.grant.ClientCredentialsGrant())
auth_controller.add_grant(oauth2.grant.RefreshToken(expires_in=3600))
auth_controller.add_grant(oauth2.grant.ResourceOwnerGrant(site_adapter=site_adapter))
......
......@@ -27,9 +27,8 @@ from zoe_api.exceptions import ZoeRestAPIException, ZoeNotFoundException, ZoeAut
from zoe_api.auth.ldap import LDAPAuthenticator
from zoe_api.auth.file import PlainTextAuthenticator
from zoe_api.auth.ldapsasl import LDAPSASLAuthenticator
from zoe_api.auth.base import BaseAuthenticator # pylint: disable=unused-import
from zoe_api.rest_api.oauth_utils import auth_controller, mongo
from zoe_api.auth.base import BaseAuthenticator
from zoe_api.rest_api.oauth_utils import auth_controller, client_store, token_store
import json
import time
......@@ -86,21 +85,19 @@ def get_auth(handler: tornado.web.RequestHandler):
#Process for authentication with token
if "Bearer" in auth_header:
token = auth_header[7:]
key = 'oauth2_{}'.format(token)
access = auth_controller.access_token_store.rs.get(key)
if access:
access = json.loads(access.decode())
username = access['client_id']
passwords = mongo['db']['oauth_clients'].find({'identifier':username})
password = ''
for p in passwords:
password = p['secret']
data = token_store.get_client_id_by_access_token(token)
if data:
uid = data["client_id"]
role = client_store.get_role_by_client_id(uid)
else:
raise ZoeRestAPIException('Invalid Token', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
if access['expires_at'] <= int(time.time()):
if int(data['expires_at'].timestamp()) <= int(time.time()):
raise ZoeRestAPIException('Expired token', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
return uid, role
#Process for authentication with username, password
else:
auth_decoded = base64.decodebytes(bytes(auth_header[6:], 'ascii')).decode('utf-8')
......
......@@ -180,3 +180,63 @@ class SQLManager:
cur.execute(query)
self.conn.commit()
return cur.fetchone()[0]
""" The above section is used for Oauth2 authentication mechanism """
def fetch_by_refresh_token(self, refresh_token):
cur = self._cursor()
query = 'SELECT * FROM oauth_token WHERE refresh_token = %s'
cur.execute(query, (refresh_token,))
return cur.fetchone()
def delete_refresh_token(self, refresh_token):
cur = self._cursor()
check_exists = 'SELECT * FROM oauth_token WHERE refresh_token = %s OR token = %s'
cur.execute(check_exists, (refresh_token, refresh_token))
res = 0
if cur.fetchone():
res = 1
query = 'DELETE FROM oauth_token WHERE refresh_token = %s OR token = %s'
cur.execute(query, (refresh_token, refresh_token))
self.conn.commit()
return res
def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
cur = self._cursor()
query = 'SELECT * FROM oauth_token WHERE client_id = %s AND grant_type = %s AND user_id = %s'
cur.execute(query, (client_id, grant_type, user_id,))
return cur.fetchone()
def get_client_id_by_access_token(self, access_token):
cur = self._cursor()
query = 'SELECT * FROM oauth_token WHERE token = %s'
cur.execute(query, (access_token,))
return cur.fetchone()
def save_token(self, client_id, grant_type, token, data, expires_at, refresh_token, refresh_expires_at, scopes, user_id):
cur = self._cursor()
expires_at = datetime.datetime.fromtimestamp(expires_at)
if refresh_expires_at == None:
query = cur.mogrify('UPDATE oauth_token SET token = %s, expires_at = %s WHERE client_id=%s', (token, expires_at, client_id))
else:
refresh_token_expires_at = datetime.datetime.fromtimestamp(refresh_expires_at)
query = cur.mogrify('INSERT INTO oauth_token (client_id, grant_type, token, data, expires_at, refresh_token, refresh_token_expires_at, scopes, user_id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (client_id) DO UPDATE SET token = %s, expires_at = %s, refresh_token = %s, refresh_token_expires_at = %s', (client_id, grant_type, token, data, expires_at, refresh_token, refresh_token_expires_at, scopes, user_id, token, expires_at, refresh_token, refresh_token_expires_at))
cur.execute(query)
self.conn.commit()
def save_client(self, identifier, secret, role, redirect_uris, authorized_grants, authorized_response_types):
cur = self._cursor()
query = cur.mogrify('INSERT INTO oauth_client (identifier, secret, role, redirect_uris, authorized_grants, authorized_response_types) VALUES (%s,%s,%s,%s,%s,%s)', (identifier, secret, role, redirect_uris, authorized_grants, authorized_response_types))
cur.execute(query)
self.conn.commit()
def fetch_by_client_id(self, client_id):
cur = self._cursor()
query = 'SELECT * FROM oauth_client WHERE identifier = %s'
cur.execute(query, (client_id,))
return cur.fetchone()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment