-
Notifications
You must be signed in to change notification settings - Fork 471
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding Authentication handlers for API and Web Server (#1105)
* Adding Authentication handlers for API and Web Server * Updated the requirements/base.txt * Added jwt package in requirements/base.txt * Modified as per the suggestions * changes made as per suggestions * More changes as suggested
- Loading branch information
Showing
10 changed files
with
471 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
""" | ||
owtf.api.handlers.api_token | ||
~~~~~~~~~~~~~~~~~~~~~~~~ | ||
""" | ||
from owtf.api.handlers.base import APIRequestHandler | ||
from owtf.api.handlers.jwtauth import jwtauth | ||
from uuid import uuid4 | ||
from owtf.models.api_token import ApiToken | ||
import jwt | ||
from owtf.settings import JWT_SECRET_KEY, JWT_OPTIONS | ||
from owtf.lib.exceptions import APIError | ||
|
||
|
||
@jwtauth | ||
class ApiTokenGenerateHandler(APIRequestHandler): | ||
"""Create the api token for a user.""" | ||
|
||
SUPPORTED_METHODS = ["GET"] | ||
|
||
def get(self): | ||
"""Get api token for a logged in user | ||
**Example request**: | ||
.. sourcecode:: http | ||
GET /api/v1/generate/api_token/ | ||
Accept: application/json, text/javascript, */* | ||
X-Requested-With: XMLHttpRequest | ||
**Example response**: | ||
.. sourcecode:: http | ||
**ApiToken successful response**; | ||
HTTP/1.1 200 OK | ||
Content-Encoding: gzip | ||
Vary: Accept-Encoding | ||
Content-Type: application/json | ||
{ | ||
"status": "success", | ||
"data": { | ||
"api_key": "b9e7157c-2150-4e34-b3f1-1777a75debb7" | ||
} | ||
} | ||
""" | ||
api_key = str(uuid4()) | ||
try: | ||
token = self.request.headers.get("Authorization").split()[1] | ||
payload = jwt.decode(token, JWT_SECRET_KEY, options=JWT_OPTIONS) | ||
user_id = payload.get("user_id", None) | ||
if not user_id: | ||
raise APIError(400, "Invalid User Id") | ||
ApiToken.add_api_token(self.session, api_key, user_id) | ||
data = {"api_key": api_key} | ||
return self.success(data) | ||
except Exception: | ||
raise APIError(400, "Invalid Token") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
""" | ||
owtf.api.handlers.auth | ||
~~~~~~~~~~~~~~~~~~~~~~~~ | ||
""" | ||
from owtf.models.user_login_token import UserLoginToken | ||
from owtf.api.handlers.base import APIRequestHandler | ||
from owtf.lib.exceptions import APIError | ||
from owtf.models.user import User | ||
from datetime import datetime, timedelta | ||
import bcrypt | ||
import json | ||
import jwt | ||
import re | ||
from owtf.settings import ( | ||
JWT_SECRET_KEY, | ||
JWT_ALGORITHM, | ||
JWT_EXP_DELTA_SECONDS, | ||
is_password_valid_regex, | ||
is_email_valid_regex, | ||
) | ||
from owtf.db.session import Session | ||
|
||
|
||
class LogInHandler(APIRequestHandler): | ||
"""LogIn using the correct credentials (email, password). After successfull login a JWT Token is generated""" | ||
|
||
SUPPORTED_METHODS = ["POST"] | ||
|
||
def post(self): | ||
""" | ||
**Example request**: | ||
.. sourcecode:: http | ||
POST /api/v1/login/ HTTP/1.1 | ||
Content-Type: application/json; charset=UTF-8 | ||
{ | ||
"email": "test@test.com", | ||
"password": "Test@34335", | ||
} | ||
**Example response**: | ||
.. sourcecode:: http | ||
**Login successful response**; | ||
HTTP/1.1 200 OK | ||
Content-Encoding: gzip | ||
Vary: Accept-Encoding | ||
Content-Type: application/json; charset=UTF-8 | ||
{ | ||
"status": "success", | ||
"data": { | ||
"jwt-token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozNSwiZXhwIjoxNjIzMjUyMjQwfQ.FjTpJySn3wprlaS26dC9LGBOMrtHJeJsTDJnyCKNmBk" | ||
} | ||
} | ||
**Login failed response**; | ||
HTTP/1.1 200 OK | ||
Content-Encoding: gzip | ||
Vary: Accept-Encoding | ||
Content-Type: application/json; charset=UTF-8 | ||
{ | ||
"status": "fail", | ||
"data": "Invalid login credentials" | ||
} | ||
""" | ||
body_data = json.loads(self.request.body.decode("utf-8")) | ||
email = body_data.get("email", None) | ||
password = body_data.get("password", None) | ||
if not email: | ||
raise APIError(400, "Missing email value") | ||
if not password: | ||
raise APIError(400, "Missing password value") | ||
user = User.find_by_email(self.session, email)[0] | ||
if ( | ||
user | ||
and user.password | ||
and bcrypt.hashpw(password.encode("utf-8"), user.password.encode("utf-8")) == user.password.encode("utf-8") | ||
): | ||
payload = {"user_id": user.id, "exp": datetime.utcnow() + timedelta(seconds=JWT_EXP_DELTA_SECONDS)} | ||
jwt_token = jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM) | ||
data = {"jwt-token": jwt_token.decode("utf-8")} | ||
UserLoginToken.add_user_login_token(self.session, jwt_token, user.id) | ||
self.success(data) | ||
else: | ||
raise APIError(400, "Invalid login credentials") | ||
|
||
|
||
class RegisterHandler(APIRequestHandler): | ||
"""Registers a new user when he provides email, name, password and confirm password""" | ||
|
||
SUPPORTED_METHODS = ["POST"] | ||
|
||
def post(self): | ||
""" | ||
**Example request**: | ||
.. sourcecode:: http | ||
POST /api/v1/register/ HTTP/1.1 | ||
Content-Type: application/json; charset=UTF-8 | ||
{ | ||
"email": "test@test.com", | ||
"password": "Test@34335", | ||
"confirm_password": "Test@34335", | ||
"name": "test" | ||
} | ||
**Example response**: | ||
.. sourcecode:: http | ||
**Successful registration response** | ||
HTTP/1.1 200 OK | ||
Content-Encoding: gzip | ||
Vary: Accept-Encoding | ||
Content-Type: application/json; charset=UTF-8 | ||
{ | ||
"status": "success", | ||
"data": "User created successfully" | ||
} | ||
**Failed registration response** | ||
HTTP/1.1 200 OK | ||
Content-Encoding: gzip | ||
Vary: Accept-Encoding | ||
Content-Type: application/json; charset=UTF-8 | ||
{ | ||
"status": "fail", | ||
"data": "Email already exists" | ||
} | ||
""" | ||
body_data = json.loads(self.request.body.decode("utf-8")) | ||
name = body_data.get("name", None) | ||
email = body_data.get("email", None) | ||
password = body_data.get("password", None) | ||
confirm_password = body_data.get("confirm_password", None) | ||
|
||
if not name: | ||
raise APIError(400, "Missing username value") | ||
if not email: | ||
raise APIError(400, "Missing email value") | ||
if not password: | ||
raise APIError(400, "Missing password value") | ||
if not confirm_password: | ||
raise APIError(400, "Missing confirm password value") | ||
|
||
already_taken = User.find_by_email(self.session, email) | ||
match_password = re.search(is_password_valid_regex, password) | ||
match_email = re.search(is_email_valid_regex, email) | ||
|
||
if password != confirm_password: | ||
raise APIError(400, "Password doesn't match") | ||
elif not match_email: | ||
raise APIError(400, "Choose a valid email") | ||
elif not match_password: | ||
raise APIError(400, "Choose a strong password") | ||
elif already_taken: | ||
raise APIError(400, "Email already exists") | ||
else: | ||
hashed_pass = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) | ||
user = {} | ||
user["email"] = email | ||
user["password"] = hashed_pass | ||
user["name"] = name | ||
User.add_user(self.session, user) | ||
data = "User created successfully" | ||
self.success(data) | ||
|
||
|
||
class LogOutHandler(APIRequestHandler): | ||
"""Logs out the current user and clears the cookie""" | ||
|
||
def get(self): | ||
""" | ||
**Example request**: | ||
.. sourcecode:: http | ||
GET /api/v1/logout/ HTTP/1.1 | ||
**Example response**: | ||
.. sourcecode:: http | ||
HTTP/1.1 200 OK | ||
Content-Encoding: gzip | ||
Vary: Accept-Encoding | ||
Content-Type: application/json; charset=UTF-8 | ||
{ | ||
"status": "success", | ||
"data": { | ||
"status": "ok" | ||
} | ||
} | ||
""" | ||
auth = self.request.headers.get("Authorization") | ||
if auth: | ||
parts = auth.split() | ||
token = parts[1] | ||
session = Session() | ||
UserLoginToken.delete_user_login_token(session, token) | ||
data = "Logged out" | ||
self.success(data) | ||
else: | ||
raise APIError(400, "Invalid Token") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
""" | ||
JSON Web Token auth for Tornado | ||
""" | ||
from sqlalchemy.sql.functions import user | ||
from owtf.models.user_login_token import UserLoginToken | ||
import jwt | ||
from owtf.settings import JWT_SECRET_KEY, JWT_OPTIONS | ||
from owtf.db.session import Session | ||
|
||
|
||
def jwtauth(handler_class): | ||
"""Handle Tornado JWT Auth""" | ||
|
||
def wrap_execute(handler_execute): | ||
def require_auth(handler, kwargs): | ||
|
||
auth = handler.request.headers.get("Authorization") | ||
if auth: | ||
parts = auth.split() | ||
|
||
if parts[0].lower() != "bearer" or len(parts) == 1 or len(parts) > 2: | ||
handler._transforms = [] | ||
handler.set_status(401) | ||
handler.write("invalid header authorization") | ||
handler.finish() | ||
|
||
token = parts[1] | ||
try: | ||
payload = jwt.decode(token, JWT_SECRET_KEY, options=JWT_OPTIONS) | ||
user_id = payload.get("user_id", None) | ||
session = Session() | ||
user_token = UserLoginToken.find_by_userid_and_token(session, user_id, token) | ||
if user_id is None or user_token is None: | ||
handler._transforms = [] | ||
handler.set_status(401) | ||
handler.write("Unauthorized") | ||
handler.finish() | ||
|
||
except Exception: | ||
handler._transforms = [] | ||
handler.set_status(401) | ||
handler.write("Unauthorized") | ||
handler.finish() | ||
else: | ||
handler._transforms = [] | ||
handler.write("Missing authorization") | ||
handler.finish() | ||
|
||
return True | ||
|
||
def _execute(self, transforms, *args, **kwargs): | ||
|
||
try: | ||
require_auth(self, kwargs) | ||
except Exception: | ||
return False | ||
|
||
return handler_execute(self, transforms, *args, **kwargs) | ||
|
||
return _execute | ||
|
||
handler_class._execute = wrap_execute(handler_class._execute) | ||
return handler_class |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
""" | ||
owtf.models.api_token | ||
~~~~~~~~~~~~~~~~ | ||
""" | ||
from sqlalchemy import Column, Integer, Unicode, ForeignKey | ||
from owtf.db.model_base import Model | ||
import uuid | ||
|
||
|
||
class ApiToken(Model): | ||
__tablename__ = "api_tokens" | ||
|
||
id = Column(Integer, primary_key=True, autoincrement=True) | ||
user_id = Column(Integer, ForeignKey("users.id")) | ||
key = Column(Unicode(255), nullable=False) | ||
|
||
@classmethod | ||
def find_by_userid(cls, session, user_id): | ||
"""Find a api_token by user_id. | ||
Returns None if not found. | ||
""" | ||
return session.query(ApiToken).filter_by(user_id=user_id).all() | ||
|
||
@classmethod | ||
def add_api_token(cls, session, key, user_id): | ||
"""Adds an api_token to the DB""" | ||
new_token = cls(user_id=user_id, key=key) | ||
session.add(new_token) | ||
session.commit() |
Oops, something went wrong.