From 83889b8293f7f27bd8a0d7155443d65a1d834b0e Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Dec 19 2017 13:29:12 +0000
Subject: [PATCH 1/10] Add user API CRUD


Signed-off-by: danmoura17 <danmoura17@gmail.com>
Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>

---

diff --git a/alembic/versions/cecf8298b202_added_user_table.py b/alembic/versions/cecf8298b202_added_user_table.py
new file mode 100644
index 0000000..2a18487
--- /dev/null
+++ b/alembic/versions/cecf8298b202_added_user_table.py
@@ -0,0 +1,37 @@
+"""Added user table.
+
+Revision ID: cecf8298b202
+Revises: 9ee67bf38f1f
+Create Date: 2017-11-09 16:50:47.759236
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'cecf8298b202'
+down_revision = '9ee67bf38f1f'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    """TODO: Add an upgrade description."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('users',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.Unicode(length=255), nullable=False),
+    sa.Column('email', sa.String(length=255), nullable=False),
+    sa.Column('password_hash', sa.String(length=128), nullable=True),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('email')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    """TODO: Add a downgrade description."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('users')
+    # ### end Alembic commands ###
diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 2564bf2..0304b0e 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -1,12 +1,13 @@
 """kiskadee API."""
-from flask import Flask, jsonify
+from flask import Flask, jsonify, abort, make_response
 from flask import request
 from flask_cors import CORS
+from marshmallow.exceptions import ValidationError
 
 from kiskadee.database import Database
-from kiskadee.model import Package, Fetcher, Version, Analysis
+from kiskadee.model import Package, Fetcher, Version, Analysis, User
 from kiskadee.api.serializers import PackageSchema, FetcherSchema,\
-        AnalysisSchema
+        AnalysisSchema, UserSchema
 import json
 from sqlalchemy.orm import eagerload
 
@@ -105,6 +106,149 @@ def analysis_reports(pkg_name, version, analysis_id):
     return jsonify({'analysis_report': report})
 
 
+@kiskadee.route('/users', methods=['GET'])
+def get_users():
+    """Get the list of users
+
+    GET /users
+
+    Possible status code:
+        - 200 Ok -> Users list
+    """
+    db_session = kiskadee_db_session()
+    users = db_session.query(User).all()
+    user_schema = UserSchema(many=True)
+    result = user_schema.dump(users)
+
+    return make_response(jsonify({'users': result.data}), 200)
+
+
+@kiskadee.route('/users', methods=['POST'])
+def create_user():
+    """Create a new user
+
+    POST /users
+
+    Possible status code:
+        - 201 Created -> User created
+        - 400 Bad Request -> Validation error
+        - 403 Forbidden -> User already exists
+    """
+    db_session = kiskadee_db_session()
+    data = request.get_json()
+
+    # Verify is user already exists
+    if data.get('email'):
+        user = db_session.query(User).filter_by(email=data.get('email')).first()
+
+        if user is not None:
+            return make_response(jsonify({'error': 'user already exists'}), 403)
+
+    # Try to create user
+    try:
+        user = UserSchema.create(**data)
+    except ValidationError as error:
+        return make_response(jsonify({
+            'error': 'Validation error',
+            'validations': error.args[0]
+        }), 400)
+
+    db_session.add(user)
+    db_session.commit()
+
+    user_schema = UserSchema()
+    result = user_schema.dump(user)
+
+    return make_response(jsonify({'user': result.data}), 201)
+
+
+@kiskadee.route('/users/<user_id>', methods=['GET'])
+def get_user_data(user_id):
+    """Get the user data
+
+    GET /users/:id
+
+    Possible status code:
+        - 200 Ok -> User data
+        - 404 Not Found -> User not found
+    """
+    db_session = kiskadee_db_session()
+    user = db_session.query(User).filter_by(id=user_id).first()
+
+    if user is None:
+        return make_response(jsonify({'error': 'user not found'}), 404)
+
+    user_schema = UserSchema()
+    result = user_schema.dump(user)
+
+    return make_response(jsonify({'user': result.data}), 200)
+
+
+@kiskadee.route('/users/<user_id>', methods=['PUT'])
+def update_user(user_id):
+    """Updates a user
+
+    PUT /users/:id
+
+    Possible status code:
+        - 200 Ok -> User updated
+        - 400 Bad Request -> Validation error
+        - 404 Not Found -> User not found
+    """
+    db_session = kiskadee_db_session()
+    user = db_session.query(User).filter_by(id=user_id).first()
+
+    if user is None:
+        return make_response(jsonify({'error': 'user not found'}), 404)
+
+    json_data = request.get_json()
+    user_data = UserSchema().dump(user).data
+    user_data.update(json_data)
+
+    validation = UserSchema().load(user_data)
+
+    if bool(validation.errors):
+        return make_response(jsonify({
+            'error': 'Validation error',
+            'validations': validation.errors
+        }), 400)
+
+    password = validation.data.get('validation')
+    if password is not None:
+        user.hash_password(password)
+        del validation.data['password']
+
+    for (key, value) in validation.data.items():
+        setattr(user, key, value)
+
+    db_session.commit()
+
+    result = UserSchema().dump(user)
+    return make_response(jsonify({'user': result.data}), 200)
+
+
+@kiskadee.route('/users/<user_id>', methods=['DELETE'])
+def delete_user(user_id):
+    """Deletes a user
+
+    DELETE /users/:id
+
+    Possible status code:
+        - 204 No Content -> User deleted
+        - 404 Not Found -> User not found
+    """
+    db_session = kiskadee_db_session()
+    user = db_session.query(User).filter_by(id=user_id).first()
+
+    if user is None:
+        return make_response(jsonify({'error': 'user not found'}), 404)
+
+    db_session.delete(user)
+    db_session.commit()
+
+    return make_response(jsonify({}), 204)
+
+
 def kiskadee_db_session():
     """Return a kiskadee database session."""
     return Database().session
diff --git a/kiskadee/api/serializers.py b/kiskadee/api/serializers.py
index e7168e9..44e2f0a 100644
--- a/kiskadee/api/serializers.py
+++ b/kiskadee/api/serializers.py
@@ -1,8 +1,8 @@
 """Provide objects to serialize the kiskadee models."""
 
-from marshmallow import Schema, fields
+from marshmallow import Schema, fields, validate, exceptions
 from kiskadee.model import Package, Fetcher, Analysis, Version,\
-        Report, Analyzer
+        Report, Analyzer, User
 
 
 class ReportsSchema(Schema):
@@ -88,3 +88,51 @@ class PackageSchema(Schema):
         """Serialize a Package object."""
         print('MAKING OBJECT FROM', data)
         return Package(**data)
+
+
+class UserSchema(Schema):
+    """Provide a serializer to the User model."""
+
+    id = fields.Int(dump_only=True)
+    name = fields.Str(required=True, validate=validate.Length(min=4, max=255))
+    email = fields.Str(
+        required=True,
+        validate=[validate.Email(error='Not a valid email address'),
+                  validate.Length(min=4, max=255)])
+    password = fields.Str(load_only=True,
+                          validate=validate.Length(min=4, max=255))
+
+    def make_object(self, data):
+        """Serialize a User object."""
+        print('MAKING OBJECT FROM', data)
+        return User(**data)
+
+    @classmethod
+    def create(cls, **data):
+        """User factory that creates a User object without saving it.
+
+        If the given data has errors it raises an
+        marshmallow.exceptions.ValidationError, else create an User model
+        and return it widthout saving.
+
+        :data: User model attributes
+        """
+        validation = UserSchema().load(data)
+
+        if bool(validation.errors):
+            raise exceptions.ValidationError(validation.errors)
+
+        password = validation.data.get('password')
+        if password is not None:
+            del validation.data['password']
+
+        user = User(**validation.data)
+
+        if password is not None:
+            user.hash_password(str(password))
+        else:
+            raise exceptions.ValidationError({
+                'password': 'Missing data for required field.'
+            })
+
+        return user
\ No newline at end of file
diff --git a/kiskadee/model.py b/kiskadee/model.py
index 6aa3233..86f2365 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -2,7 +2,8 @@
 
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy import Column, Integer, UnicodeText, UniqueConstraint,\
-                       Sequence, Unicode, ForeignKey, orm, JSON
+                       Sequence, Unicode, ForeignKey, orm, JSON, String
+from passlib.apps import custom_app_context as pwd_context
 import kiskadee
 
 Base = declarative_base()
@@ -105,3 +106,25 @@ def create_analyzers(_session):
             new_analyzer.version = version
             _session.add(new_analyzer)
     _session.commit()
+
+
+class User(Base):
+    __tablename__ = 'users'
+    id = Column(Integer,
+                Sequence('users_id_seq', optional=True), primary_key=True)
+    name = Column(Unicode(255), nullable=False)
+    email = Column(String(255), nullable=False, unique=True)
+    password_hash = Column(String(128))
+
+    def hash_password(self, password):
+        """Takes a plain password as argument
+        and stores a hash of it with the user.
+        """
+        self.password_hash = pwd_context.hash(password)
+
+    def verify_password(self, password):
+        """Takes a plain password as argument and returns
+        True if the password is correct
+        False if not.
+        """
+        return pwd_context.verify(password, self.password_hash)
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
new file mode 100644
index 0000000..70e24c8
--- /dev/null
+++ b/kiskadee/tests/test_api_users.py
@@ -0,0 +1,318 @@
+import json
+import unittest
+from sqlalchemy.orm import sessionmaker
+from unittest.mock import patch
+
+import kiskadee
+import kiskadee.api.app
+
+from kiskadee.model import User
+from kiskadee.api.serializers import UserSchema
+
+def mock_hash_password(self, password):
+    """Mock for User model hash_password method.
+    It is too slow for the tests.
+    """
+    self.password_hash = str(password)
+
+
+class ApiUsersTestCase(unittest.TestCase):
+
+    def setUp(self):
+        kiskadee.api.app.kiskadee.testing = True
+        self.engine = kiskadee.database.Database('db_test').engine
+        Session = sessionmaker(bind=self.engine)
+        self.session = Session()
+        self.app = kiskadee.api.app.kiskadee.test_client()
+
+        kiskadee.model.Base.metadata.create_all(self.engine)
+        self._setup_mock_users()
+
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def _setup_mock_users(self):
+        mock_users_data = [
+            {'name': 'test 1', 'email': 'test@user1.com', 'password': 'test'},
+            {'name': 'test 2', 'email': 'test@user2.com', 'password': 'test'},
+            {'name': 'test 3', 'email': 'test@user3.com', 'password': 'test'}
+        ]
+
+        for mock_data in mock_users_data:
+            user = UserSchema.create(**mock_data)
+            self.session.add(user)
+
+        self.session.commit()
+
+    def tearDown(self):
+        self.session.close()
+        kiskadee.model.Base.metadata.drop_all()
+
+    # GET /users -> 200 ok
+    def test_get_users(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.get("/users")
+        data = json.loads(response.data.decode("utf-8"))
+        total_users_count = self.session.query(User).count()
+
+        self.assertIn("users", data)
+        self.assertEqual(len(data['users']), total_users_count)
+        self.assertEqual(200, response.status_code)
+
+        # no password field is given
+        user = data['users'][0]
+        self.assertIsNone(user.get('password'))
+        self.assertIsNone(user.get('password_hash'))
+
+
+    # POST /users -> 201 created
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_successful_create_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        new_user_data = {
+            'name': 'new user',
+            'email': 'new@user.com',
+            'password': 'new user'
+        }
+
+        total_users_before_creation = self.session.query(User).count()
+        response = self.app.post("/users",
+                                 data=json.dumps(new_user_data),
+                                 content_type='application/json')
+        total_users_after_creation = self.session.query(User).count()
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(total_users_after_creation, total_users_before_creation + 1)
+        self.assertEqual(data['user']['email'], new_user_data['email'])
+        self.assertEqual(201, response.status_code)
+
+    # POST /users -> 400 Bad Request
+    def test_missing_arguments_create_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        new_user_data = {
+            'name': 'new user',
+            'password': 'new user'
+        }
+
+        total_users_before_creation = self.session.query(User).count()
+        response = self.app.post("/users",
+                                 data=json.dumps(new_user_data),
+                                 content_type='application/json')
+        total_users_after_creation = self.session.query(User).count()
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertIn("validations", data)
+        self.assertEqual(total_users_after_creation, total_users_before_creation)
+        self.assertEqual(data['error'], 'Validation error')
+        self.assertEqual(data['validations']['email'][0],
+                         'Missing data for required field.')
+        self.assertEqual(400, response.status_code)
+
+    # POST /users -> 403 Forbidden
+    def test_already_exists_create_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        new_user_data = {
+            'name': 'new user',
+            'password': 'new user',
+            'email': 'test@user1.com'
+        }
+
+        total_users_before_creation = self.session.query(User).count()
+        response = self.app.post("/users",
+                                 data=json.dumps(new_user_data),
+                                 content_type='application/json')
+        total_users_after_creation = self.session.query(User).count()
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(total_users_after_creation, total_users_before_creation)
+        self.assertEqual(data['error'], 'user already exists')
+        self.assertEqual(403, response.status_code)
+
+    # GET /users/:id -> 200 ok
+    def test_get_user_data(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+
+        response = self.app.get("/users/%d" % user.id)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['email'], user.email)
+        self.assertEqual(200, response.status_code)
+
+        # no password field is given
+        self.assertIsNone(data['user'].get('password'))
+        self.assertIsNone(data['user'].get('password_hash'))
+
+    # GET /users/:id -> 404 Not Found
+    def test_not_found_get_user_data(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.get("/users/%d" % 123456789)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'], 'user not found')
+        self.assertEqual(404, response.status_code)
+
+    # PUT /users/:id -> 200 ok
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_successful_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        response = self.app.put("/users/{}".format(user.id),
+                                 data=json.dumps({'email': 'another@email.com',
+                                                  'password': 'password'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['id'], user.id)
+        self.assertEqual(data['user']['email'], 'another@email.com')
+        self.assertEqual(200, response.status_code)
+
+        updated_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertEqual(user.id, updated_user.id)
+        self.assertEqual(user.password_hash, updated_user.password_hash)
+        self.assertEqual(updated_user.email, 'another@email.com')
+
+    # PUT /users/:id -> 200 ok
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_ignores_password_hash_on_ajax_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        response = self.app.put("/users/{}".format(user.id),
+                                 data=json.dumps({'password_hash': 'ignome_me'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['id'], user.id)
+        self.assertEqual(200, response.status_code)
+
+        updated_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertNotEqual(updated_user.password_hash, 'ignome_me')
+        self.assertEqual(updated_user.password_hash, user.password_hash)
+
+    # PUT /users/:id -> 400 Bad Request
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_validation_errors_on_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        response = self.app.put("/users/{}".format(user.id),
+                                 data=json.dumps({'password': 'foo',
+                                                  'email': 'not an email'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertIn("validations", data)
+        self.assertEqual(data['error'], 'Validation error')
+        self.assertEqual(data['validations']['email'][0],
+                         'Not a valid email address')
+        self.assertEqual(data['validations']['password'][0],
+                         'Length must be between 4 and 255.')
+        self.assertEqual(400, response.status_code)
+
+        updated_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertNotEqual(updated_user.email, 'not an email')
+
+
+    # PUT /users/:id -> 404 Not Found
+    def test_not_found_update_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.put("/users/{}".format(123456789),
+                                 data=json.dumps({'password': 'not found ?'}),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'], 'user not found')
+        self.assertEqual(404, response.status_code)
+
+    # DELETE /users/:id -> 204 No Content
+    def test_successful_delete_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+
+        response = self.app.delete("/users/{}".format(user.id))
+
+        self.assertEqual(response.data, b'')
+        self.assertEqual(204, response.status_code)
+
+        deleted_user = self.session.query(User).filter_by(id=user.id).first()
+
+        self.assertIsNone(deleted_user)
+
+    # DELETE /users/:id -> 404 Not Found
+    def test_not_found_delete_user(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        response = self.app.delete("/users/{}".format(123456789))
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'], 'user not found')
+        self.assertEqual(404, response.status_code)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/kiskadee/tests/test_model.py b/kiskadee/tests/test_model.py
index 4a56835..8615c88 100644
--- a/kiskadee/tests/test_model.py
+++ b/kiskadee/tests/test_model.py
@@ -174,6 +174,22 @@ class ModelTestCase(unittest.TestCase):
         self.assertEqual(analysis[0].raw, "<>")
         self.assertEqual(analysis[1].raw, "><")
 
+    def test_it_hash_a_user_password(self):
+        u = model.User()
+
+        old_password = 'foobar'
+        u.password_hash = old_password
+        u.hash_password('test')
+
+        self.assertNotEqual(u.password_hash, old_password)
+        self.assertGreater(len(u.password_hash), 100)
+
+    def test_it_verify_a_user_password(self):
+        u = model.User()
+        u.hash_password('test')
+
+        self.assertTrue(u.verify_password('test'))
+        self.assertFalse(u.verify_password('wrong password'))
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/kiskadee/tests/test_serializers.py b/kiskadee/tests/test_serializers.py
new file mode 100644
index 0000000..a4482e8
--- /dev/null
+++ b/kiskadee/tests/test_serializers.py
@@ -0,0 +1,53 @@
+import unittest
+from marshmallow.exceptions import ValidationError
+
+from kiskadee.api.serializers import UserSchema, User
+
+
+class SerializersTestCase(unittest.TestCase):
+
+    def test_UserSchema_validates_user_data(self):
+        wrong_data = {'name': 'foo', 'email': 'foo', 'password': 'foo'}
+        validation = UserSchema().load(wrong_data)
+
+        self.assertTrue(validation.errors)
+        self.assertEqual(validation.errors['name'][0],
+                         'Length must be between 4 and 255.')
+        self.assertEqual(validation.errors['email'][0],
+                         'Not a valid email address')
+        self.assertEqual(validation.errors['email'][1],
+                         'Length must be between 4 and 255.')
+        self.assertEqual(validation.errors['password'][0],
+                         'Length must be between 4 and 255.')
+
+        validation = UserSchema().load({})
+
+        self.assertEqual(validation.errors['name'][0],
+                         'Missing data for required field.')
+
+        self.assertEqual(validation.errors['email'][0],
+                         'Missing data for required field.')
+
+    def test_UserSchema_create_a_user_instance(self):
+        data = {'name': 'Test', 'email': 'test@email.com', 'password': 'test'}
+        user = UserSchema.create(**data)
+
+        self.assertIsInstance(user, User)
+        self.assertNotEqual(user.password_hash, 'test')
+        self.assertGreater(len(user.password_hash), 100)
+
+    def test_UserSchema_raise_ValidationError_if_data_is_invalid(self):
+        with self.assertRaises(ValidationError) as context:
+            data = {}
+            UserSchema.create(**data)
+
+        errors = context.exception.args[0]
+        self.assertIsInstance(context.exception, ValidationError)
+        self.assertEqual(errors['name'][0],
+                         'Missing data for required field.')
+        self.assertEqual(errors['email'][0],
+                         'Missing data for required field.')
+
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index f1039cf..e7bf7ff 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,3 +13,5 @@ Flask-Restless
 marshmallow
 alembic
 python-debian
+passlib
+bcrypt
\ No newline at end of file

From 4c15c060f07250fba2142efe32ff6bc83a97f9ad Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Dec 19 2017 13:30:01 +0000
Subject: [PATCH 2/10] Add token generation and verification


---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 0304b0e..2064cc9 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -5,17 +5,75 @@ from flask_cors import CORS
 from marshmallow.exceptions import ValidationError
 
 from kiskadee.database import Database
-from kiskadee.model import Package, Fetcher, Version, Analysis, User
+from kiskadee.model import Package, Fetcher, Version, Analysis, User,\
+        TOKEN_SECRET_KEY
 from kiskadee.api.serializers import PackageSchema, FetcherSchema,\
         AnalysisSchema, UserSchema
 import json
 from sqlalchemy.orm import eagerload
 
+import jwt
+from functools import wraps
+
 kiskadee = Flask(__name__)
 
 CORS(kiskadee)
 
 
+def token_required(fn):
+    """Token verification decorator. When applyed on a route it will
+    look for the x-access-token on the request header.
+
+    If it is valid, the the route is executed.
+    Else, the token is missing or is invalid or has expired, either way
+    the user receive a 403 status code when invalid.
+
+    Possible status code:
+        - 403 Forbidden ->
+            "Token is missing" or "Token expired" or "Invalid token"
+    """
+    @wraps(fn)
+    def decorated(*args, **kwargs):
+        token = None
+
+        if 'x-access-token' in request.headers:
+            token = request.headers['x-access-token']
+
+        if not token:
+            return make_response(jsonify({'error': 'Token is missing'}), 403)
+
+        try:
+            data = jwt.decode(token, TOKEN_SECRET_KEY)
+        except jwt.ExpiredSignatureError:
+            return make_response(jsonify({
+                    'error': 'Token expired'
+                    }), 403)
+        except jwt.InvalidTokenError:
+            return make_response(jsonify({
+                    'error': 'Invalid token'
+                    }), 403)
+
+        params = dict(kwargs, token_data=data)
+        return fn(*args, **params)
+
+    return decorated
+
+@kiskadee.route('/login', methods=['POST'])
+def login():
+    json_data = request.get_json()
+    email, password = [json_data.get('email'), json_data.get('password')]
+
+    if email and password:
+        db_session = kiskadee_db_session()
+        user = db_session.query(User).filter_by(email=email).first()
+
+        if user is not None and user.verify_password(password):
+            token = user.generate_token()
+            return make_response(jsonify({'token': token}), 200)
+
+    return make_response(jsonify({'error': 'Could not verify !'}), 401)
+
+
 @kiskadee.route('/fetchers')
 def index():
     """Get the list of available fetchers."""
@@ -107,7 +165,8 @@ def analysis_reports(pkg_name, version, analysis_id):
 
 
 @kiskadee.route('/users', methods=['GET'])
-def get_users():
+@token_required
+def get_users(token_data):
     """Get the list of users
 
     GET /users
diff --git a/kiskadee/model.py b/kiskadee/model.py
index 86f2365..b6cab39 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -6,8 +6,13 @@ from sqlalchemy import Column, Integer, UnicodeText, UniqueConstraint,\
 from passlib.apps import custom_app_context as pwd_context
 import kiskadee
 
-Base = declarative_base()
+import jwt
+import os
+import datetime
+
+TOKEN_SECRET_KEY = os.getenv('TOKEN_SECRET_KEY', 'default development key')
 
+Base = declarative_base()
 
 class Package(Base):
     """Software packages abstraction.
@@ -128,3 +133,12 @@ class User(Base):
         False if not.
         """
         return pwd_context.verify(password, self.password_hash)
+
+    def generate_token(self):
+        """Generates user auth token and returns it"""
+        token = jwt.encode({
+            'user_id': self.id,
+            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
+        }, TOKEN_SECRET_KEY)
+
+        return token.decode('UTF-8')
\ No newline at end of file
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index 70e24c8..b8862a9 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -53,7 +53,11 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
-        response = self.app.get("/users")
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        response = self.app.get("/users",
+                                headers={'x-access-token': user_token})
         data = json.loads(response.data.decode("utf-8"))
         total_users_count = self.session.query(User).count()
 
diff --git a/kiskadee/tests/test_model.py b/kiskadee/tests/test_model.py
index 8615c88..0ea53f0 100644
--- a/kiskadee/tests/test_model.py
+++ b/kiskadee/tests/test_model.py
@@ -1,6 +1,7 @@
 import unittest
 from sqlalchemy import exc
 from sqlalchemy.orm import sessionmaker
+import jwt
 
 from kiskadee import model
 from kiskadee.database import Database
@@ -191,5 +192,20 @@ class ModelTestCase(unittest.TestCase):
         self.assertTrue(u.verify_password('test'))
         self.assertFalse(u.verify_password('wrong password'))
 
+    def test_it_generates_a_user_auth_token(self):
+        u = model.User(name='test', email='test@email.com')
+        u.hash_password('test')
+
+        self.session.add(u)
+        self.session.commit()
+
+        u = self.session.query(model.User)\
+                .filter_by(email='test@email.com').first()
+        token = u.generate_token()
+        decoded_token = jwt.decode(token, model.TOKEN_SECRET_KEY)
+
+        self.assertGreaterEqual(len(token), 121)
+        self.assertEqual(decoded_token['user_id'], u.id)
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/requirements.txt b/requirements.txt
index e7bf7ff..a2431b9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,4 +14,5 @@ marshmallow
 alembic
 python-debian
 passlib
-bcrypt
\ No newline at end of file
+bcrypt
+pyjwt

From 6ebef8edc48722d623969b337dc9aa14bba0877f Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Dec 19 2017 13:30:38 +0000
Subject: [PATCH 3/10] Add token_required to user routes


Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>
Signed-off-by: danmoura17 <danmoura17@gmail.com>

---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 2064cc9..728d515 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -45,13 +45,9 @@ def token_required(fn):
         try:
             data = jwt.decode(token, TOKEN_SECRET_KEY)
         except jwt.ExpiredSignatureError:
-            return make_response(jsonify({
-                    'error': 'Token expired'
-                    }), 403)
+            return make_response(jsonify({'error': 'Token expired'}), 403)
         except jwt.InvalidTokenError:
-            return make_response(jsonify({
-                    'error': 'Invalid token'
-                    }), 403)
+            return make_response(jsonify({'error': 'Invalid token'}), 403)
 
         params = dict(kwargs, token_data=data)
         return fn(*args, **params)
@@ -60,6 +56,14 @@ def token_required(fn):
 
 @kiskadee.route('/login', methods=['POST'])
 def login():
+    """Token based login
+
+    POST /login
+
+    Possible status code:
+        - 200 Ok -> User token
+        - 401 Unauthorized -> Could not log user
+    """
     json_data = request.get_json()
     email, password = [json_data.get('email'), json_data.get('password')]
 
@@ -221,8 +225,9 @@ def create_user():
     return make_response(jsonify({'user': result.data}), 201)
 
 
-@kiskadee.route('/users/<user_id>', methods=['GET'])
-def get_user_data(user_id):
+@kiskadee.route('/users/<int:user_id>', methods=['GET'])
+@token_required
+def get_user_data(token_data, user_id):
     """Get the user data
 
     GET /users/:id
@@ -243,8 +248,9 @@ def get_user_data(user_id):
     return make_response(jsonify({'user': result.data}), 200)
 
 
-@kiskadee.route('/users/<user_id>', methods=['PUT'])
-def update_user(user_id):
+@kiskadee.route('/users/<int:user_id>', methods=['PUT'])
+@token_required
+def update_user(token_data, user_id):
     """Updates a user
 
     PUT /users/:id
@@ -252,6 +258,7 @@ def update_user(user_id):
     Possible status code:
         - 200 Ok -> User updated
         - 400 Bad Request -> Validation error
+        - 403 Forbidden -> Token user does not match to requested user
         - 404 Not Found -> User not found
     """
     db_session = kiskadee_db_session()
@@ -260,6 +267,11 @@ def update_user(user_id):
     if user is None:
         return make_response(jsonify({'error': 'user not found'}), 404)
 
+    if token_data['user_id'] != user_id:
+        return make_response(jsonify({
+            'error': 'token user does not match to requested user'
+            }), 403)
+
     json_data = request.get_json()
     user_data = UserSchema().dump(user).data
     user_data.update(json_data)
@@ -286,14 +298,16 @@ def update_user(user_id):
     return make_response(jsonify({'user': result.data}), 200)
 
 
-@kiskadee.route('/users/<user_id>', methods=['DELETE'])
-def delete_user(user_id):
+@kiskadee.route('/users/<int:user_id>', methods=['DELETE'])
+@token_required
+def delete_user(token_data, user_id):
     """Deletes a user
 
     DELETE /users/:id
 
     Possible status code:
         - 204 No Content -> User deleted
+        - 403 Forbidden -> Token user does not match to requested user
         - 404 Not Found -> User not found
     """
     db_session = kiskadee_db_session()
@@ -302,6 +316,11 @@ def delete_user(user_id):
     if user is None:
         return make_response(jsonify({'error': 'user not found'}), 404)
 
+    if token_data['user_id'] != user_id:
+        return make_response(jsonify({
+            'error': 'token user does not match to requested user'
+            }), 403)
+
     db_session.delete(user)
     db_session.commit()
 
diff --git a/kiskadee/model.py b/kiskadee/model.py
index b6cab39..c7bc9ca 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -132,7 +132,10 @@ class User(Base):
         True if the password is correct
         False if not.
         """
-        return pwd_context.verify(password, self.password_hash)
+        try:
+            return pwd_context.verify(password, self.password_hash)
+        except ValueError:
+            return False
 
     def generate_token(self):
         """Generates user auth token and returns it"""
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index b8862a9..2188c22 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -46,6 +46,66 @@ class ApiUsersTestCase(unittest.TestCase):
         self.session.close()
         kiskadee.model.Base.metadata.drop_all()
 
+    # POST /login -> 200 Ok
+    def test_get_user_token_on_login(self):
+        kiskadee.api.app.kiskadee_db_session = lambda: self.session
+
+        user_data = {
+            'name': 'login',
+            'email': 'login@email.com',
+            'password':'login'
+        }
+
+        # Creating a user as user.verify_password inside login route
+        # gives ValueError with users created with mock_hash_password
+        user = UserSchema.create(**user_data)
+        self.session.add(user)
+        self.session.commit()
+
+        login_data = {
+            'email': user_data['email'],
+            'password': user_data['password']
+        }
+
+        response = self.app.post("/login",
+                                 data=json.dumps(login_data),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("token", data)
+        self.assertEqual(200, response.status_code)
+
+    # POST /login -> 401 Unauthorized
+    def test_wrong_data_on_user_login_gives_unauthorized_response(self):
+        kiskadee.api.app.kiskadee_db_session = lambda: self.session
+
+        user_data = {
+            'name': 'login',
+            'email': 'login@email.com',
+            'password':'login'
+        }
+
+        user = UserSchema.create(**user_data)
+        self.session.add(user)
+        self.session.commit()
+
+        login_data = {
+            'email': user_data['email'],
+            'password': 'not my password'
+        }
+
+        response = self.app.post("/login",
+                                 data=json.dumps(login_data),
+                                 content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data["error"], "Could not verify !")
+        self.assertEqual(401, response.status_code)
+
+
     # GET /users -> 200 ok
     def test_get_users(self):
         def mock_kiskadee_db_session():
@@ -160,8 +220,10 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
 
-        response = self.app.get("/users/%d" % user.id)
+        response = self.app.get("/users/%d" % user.id,
+                                headers={'x-access-token': user_token})
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("user", data)
@@ -179,7 +241,11 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
-        response = self.app.get("/users/%d" % 123456789)
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        response = self.app.get("/users/%d" % 123456789,
+                                headers={'x-access-token': user_token})
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("error", data)
@@ -195,10 +261,13 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(user.id),
                                  data=json.dumps({'email': 'another@email.com',
                                                   'password': 'password'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -222,9 +291,12 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(user.id),
                                  data=json.dumps({'password_hash': 'ignome_me'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -246,10 +318,13 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(user.id),
                                  data=json.dumps({'password': 'foo',
                                                   'email': 'not an email'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -266,6 +341,43 @@ class ApiUsersTestCase(unittest.TestCase):
 
         self.assertNotEqual(updated_user.email, 'not an email')
 
+    # PUT /users/:id -> 403 Forbidden
+    @patch.object(User, 'hash_password', mock_hash_password)
+    def test_only_the_token_user_can_updates_its_data(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        def send_request(user, data, token):
+            return self.app.put("/users/{}".format(user.id),
+                                data=json.dumps(data),
+                                content_type='application/json',
+                                headers={'x-access-token': token})
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        user_to_update = self.session.query(User).\
+                            order_by(User.id.desc()).\
+                            first()
+
+        response = send_request(user_to_update, {'name': 'test'}, user_token)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'],
+            'token user does not match to requested user')
+        self.assertEqual(403, response.status_code)
+
+        token = user_to_update.generate_token()
+        response = send_request(user_to_update, {'name': 'new name'}, token)
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("user", data)
+        self.assertEqual(data['user']['id'], user_to_update.id)
+        self.assertEqual(data['user']['name'], 'new name')
+        self.assertEqual(200, response.status_code)
 
     # PUT /users/:id -> 404 Not Found
     def test_not_found_update_user(self):
@@ -274,9 +386,13 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
         response = self.app.put("/users/{}".format(123456789),
                                  data=json.dumps({'password': 'not found ?'}),
-                                 content_type='application/json')
+                                 content_type='application/json',
+                                 headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -292,8 +408,10 @@ class ApiUsersTestCase(unittest.TestCase):
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
         user = self.session.query(User).first()
+        user_token = user.generate_token()
 
-        response = self.app.delete("/users/{}".format(user.id))
+        response = self.app.delete("/users/{}".format(user.id),
+                                    headers={'x-access-token': user_token})
 
         self.assertEqual(response.data, b'')
         self.assertEqual(204, response.status_code)
@@ -302,6 +420,28 @@ class ApiUsersTestCase(unittest.TestCase):
 
         self.assertIsNone(deleted_user)
 
+    # DELETE /users/:id -> 403 Forbidden
+    def test_only_the_token_user_can_delete_it_self(self):
+        def mock_kiskadee_db_session():
+            return self.session
+
+        kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
+
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        other_user = self.session.query(User).order_by(User.id.desc()).first()
+
+        response = self.app.delete("/users/{}".format(other_user.id),
+                                    headers={'x-access-token': user_token})
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual(data['error'],
+            'token user does not match to requested user')
+        self.assertEqual(403, response.status_code)
+
     # DELETE /users/:id -> 404 Not Found
     def test_not_found_delete_user(self):
         def mock_kiskadee_db_session():
@@ -309,7 +449,11 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
-        response = self.app.delete("/users/{}".format(123456789))
+        user = self.session.query(User).first()
+        user_token = user.generate_token()
+
+        response = self.app.delete("/users/{}".format(123456789),
+                                    headers={'x-access-token': user_token})
 
         data = json.loads(response.data.decode("utf-8"))
 

From 61b28461a7b4950d98595b0f6e9b85413518baf2 Mon Sep 17 00:00:00 2001
From: danmoura17 <danmoura17@gmail.com>
Date: Dec 19 2017 13:30:38 +0000
Subject: [PATCH 4/10] Set auth token to expire after 48 hours


Signed-off-by: danmoura17 <danmoura17@gmail.com>
Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>

---

diff --git a/kiskadee/model.py b/kiskadee/model.py
index c7bc9ca..4e879ec 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -141,7 +141,7 @@ class User(Base):
         """Generates user auth token and returns it"""
         token = jwt.encode({
             'user_id': self.id,
-            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
+            'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=48)
         }, TOKEN_SECRET_KEY)
 
-        return token.decode('UTF-8')
\ No newline at end of file
+        return token.decode('UTF-8')

From ad4cf6142f800cf10f88498803f16ff44d86f3fe Mon Sep 17 00:00:00 2001
From: danmoura17 <danmoura17@gmail.com>
Date: Dec 19 2017 13:30:38 +0000
Subject: [PATCH 5/10] Pass user_id on login together with token


Signed-off-by: danmoura17 <danmoura17@gmail.com>
Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>

---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 728d515..711e81b 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -73,7 +73,8 @@ def login():
 
         if user is not None and user.verify_password(password):
             token = user.generate_token()
-            return make_response(jsonify({'token': token}), 200)
+            response = {'token': token, 'user_id': user.id}
+            return make_response(jsonify(response), 200)
 
     return make_response(jsonify({'error': 'Could not verify !'}), 401)
 
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index 2188c22..9955be5 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -74,6 +74,8 @@ class ApiUsersTestCase(unittest.TestCase):
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("token", data)
+        self.assertIn("user_id", data)
+        self.assertEqual(user.id, data['user_id'])
         self.assertEqual(200, response.status_code)
 
     # POST /login -> 401 Unauthorized

From 2dcbd731ec1db5752cc31abce9be004293cc1882 Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Dec 19 2017 13:30:38 +0000
Subject: [PATCH 6/10] Pass user token on creation


---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 711e81b..86d47ae 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -223,7 +223,9 @@ def create_user():
     user_schema = UserSchema()
     result = user_schema.dump(user)
 
-    return make_response(jsonify({'user': result.data}), 201)
+    token = user.generate_token()
+
+    return make_response(jsonify({'user': result.data, 'token': token}), 201)
 
 
 @kiskadee.route('/users/<int:user_id>', methods=['GET'])
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index 9955be5..d0f0383 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -156,6 +156,7 @@ class ApiUsersTestCase(unittest.TestCase):
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("user", data)
+        self.assertIn("token", data)
         self.assertEqual(total_users_after_creation, total_users_before_creation + 1)
         self.assertEqual(data['user']['email'], new_user_data['email'])
         self.assertEqual(201, response.status_code)

From 4b8303a5858a58a29c92f38574302173c9733b57 Mon Sep 17 00:00:00 2001
From: DaniloBarros <dan.b412@gmail.com>
Date: Dec 19 2017 13:30:38 +0000
Subject: [PATCH 7/10] Add fields in response of login post method


- user id, user name and user email

Signed-off-by: DaniloBarros <dan.b412@gmail.com>
Signed-off-by: thiagocmoreira <thiago.cmoreira@yahoo.com.br>

---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index 86d47ae..cafd35c 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -73,7 +73,7 @@ def login():
 
         if user is not None and user.verify_password(password):
             token = user.generate_token()
-            response = {'token': token, 'user_id': user.id}
+            response = {'token': token, 'user': {'id': user.id, 'name': user.name, 'email': user.email}}
             return make_response(jsonify(response), 200)
 
     return make_response(jsonify({'error': 'Could not verify !'}), 401)

From 6a6454ab93ea14200128a0fae54982c6e76f703f Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Dec 19 2017 13:31:56 +0000
Subject: [PATCH 8/10] Send email confirmation


---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index cafd35c..a18d7ba 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -6,9 +6,9 @@ from marshmallow.exceptions import ValidationError
 
 from kiskadee.database import Database
 from kiskadee.model import Package, Fetcher, Version, Analysis, User,\
-        TOKEN_SECRET_KEY
+    TOKEN_SECRET_KEY
 from kiskadee.api.serializers import PackageSchema, FetcherSchema,\
-        AnalysisSchema, UserSchema
+    AnalysisSchema, UserSchema
 import json
 from sqlalchemy.orm import eagerload
 
@@ -17,6 +17,8 @@ from functools import wraps
 
 kiskadee = Flask(__name__)
 
+from . import mail
+
 CORS(kiskadee)
 
 
@@ -54,6 +56,7 @@ def token_required(fn):
 
     return decorated
 
+
 @kiskadee.route('/login', methods=['POST'])
 def login():
     """Token based login
@@ -73,7 +76,8 @@ def login():
 
         if user is not None and user.verify_password(password):
             token = user.generate_token()
-            response = {'token': token, 'user': {'id': user.id, 'name': user.name, 'email': user.email}}
+            response = {'token': token, 'user': {
+                'id': user.id, 'name': user.name, 'email': user.email}}
             return make_response(jsonify(response), 200)
 
     return make_response(jsonify({'error': 'Could not verify !'}), 401)
@@ -203,7 +207,8 @@ def create_user():
 
     # Verify is user already exists
     if data.get('email'):
-        user = db_session.query(User).filter_by(email=data.get('email')).first()
+        user = db_session.query(User).filter_by(
+            email=data.get('email')).first()
 
         if user is not None:
             return make_response(jsonify({'error': 'user already exists'}), 403)
@@ -273,7 +278,7 @@ def update_user(token_data, user_id):
     if token_data['user_id'] != user_id:
         return make_response(jsonify({
             'error': 'token user does not match to requested user'
-            }), 403)
+        }), 403)
 
     json_data = request.get_json()
     user_data = UserSchema().dump(user).data
@@ -322,7 +327,7 @@ def delete_user(token_data, user_id):
     if token_data['user_id'] != user_id:
         return make_response(jsonify({
             'error': 'token user does not match to requested user'
-            }), 403)
+        }), 403)
 
     db_session.delete(user)
     db_session.commit()
diff --git a/kiskadee/api/mail.py b/kiskadee/api/mail.py
new file mode 100644
index 0000000..f5bd949
--- /dev/null
+++ b/kiskadee/api/mail.py
@@ -0,0 +1,54 @@
+import os
+import datetime
+import jwt
+from threading import Thread
+
+from flask import make_response, jsonify, url_for
+from flask_mail import Mail, Message
+
+from kiskadee import config
+from kiskadee.api.app import kiskadee
+
+kiskadee.config.update({
+    'MAIL_ENABLED': config['mail']['MAIL_ENABLED'] == 'True',
+    'MAIL_SERVER': config['mail']['MAIL_SERVER'],
+    'MAIL_PORT': config['mail']['MAIL_PORT'],
+    'MAIL_USERNAME': os.environ.get('MAIL_USERNAME'),
+    'MAIL_PASSWORD': os.environ.get('MAIL_PASSWORD'),
+    'MAIL_USE_TLS': config['mail']['MAIL_USE_TLS'] == 'True',
+    'MAIL_USE_SSL': config['mail']['MAIL_USE_SSL'] == 'True',
+    'EMAIL_TOKEN_SECRET_KEY': os.environ.get('EMAIL_TOKEN_SECRET_KEY', 'dev email token')
+})
+
+mail = Mail(kiskadee)
+
+@kiskadee.route('/users/send_email')
+def send_email():
+    token = jwt.encode({
+        'user_id': 123456,
+        'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=60)
+    }, kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
+
+    msg = Message("Sera ?", sender="fabio1079@gmail.com",
+                  recipients=['fabio1079@gmail.com'])
+    link = url_for("confirm_email", token=token, _external=True)
+    msg.body = 'MAs sera memsmo ?. Link: {}'.format(link)
+
+    mail.send(msg)
+
+    return make_response(jsonify({'ok': True}), 200)
+
+
+@kiskadee.route('/users/confirm_email/<token>')
+def confirm_email(token):
+    print("=" * 80)
+    print("Token: {}".format(token))
+    print("=" * 80)
+
+    try:
+        jwt.decode(token, kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
+        return make_response(jsonify({'ok': True}), 200)
+    except jwt.ExpiredSignatureError:
+        return make_response(jsonify({'error': 'Token expired'}), 403)
+    except jwt.InvalidTokenError:
+        return make_response(jsonify({'error': 'Invalid token'}), 403)
diff --git a/requirements.txt b/requirements.txt
index a2431b9..71de7a8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,3 +16,4 @@ python-debian
 passlib
 bcrypt
 pyjwt
+Flask-Mail
diff --git a/util/kiskadee.conf b/util/kiskadee.conf
index c2dc351..ff89280 100644
--- a/util/kiskadee.conf
+++ b/util/kiskadee.conf
@@ -52,3 +52,10 @@ cppcheck = 1.0.0
 flawfinder = 1.0.0
 clanganalyzer = 1.0.0
 frama_c = 1.0.0
+
+[mail]
+MAIL_ENABLED = True
+MAIL_SERVER = smtp.googlemail.com
+MAIL_PORT = 465
+MAIL_USE_TLS = False
+MAIL_USE_SSL = True
\ No newline at end of file

From 7d8aa57c7e5ed0badd99c47616b29a5e809b7c90 Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Dec 19 2017 13:32:42 +0000
Subject: [PATCH 9/10] Send e-mail on user registration


---

diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index a18d7ba..ec83266 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -5,16 +5,13 @@ from flask_cors import CORS
 from marshmallow.exceptions import ValidationError
 
 from kiskadee.database import Database
-from kiskadee.model import Package, Fetcher, Version, Analysis, User,\
-    TOKEN_SECRET_KEY
+from kiskadee.model import Package, Fetcher, Version, Analysis, User
 from kiskadee.api.serializers import PackageSchema, FetcherSchema,\
     AnalysisSchema, UserSchema
+from kiskadee.api.token import token_required
 import json
 from sqlalchemy.orm import eagerload
 
-import jwt
-from functools import wraps
-
 kiskadee = Flask(__name__)
 
 from . import mail
@@ -22,41 +19,6 @@ from . import mail
 CORS(kiskadee)
 
 
-def token_required(fn):
-    """Token verification decorator. When applyed on a route it will
-    look for the x-access-token on the request header.
-
-    If it is valid, the the route is executed.
-    Else, the token is missing or is invalid or has expired, either way
-    the user receive a 403 status code when invalid.
-
-    Possible status code:
-        - 403 Forbidden ->
-            "Token is missing" or "Token expired" or "Invalid token"
-    """
-    @wraps(fn)
-    def decorated(*args, **kwargs):
-        token = None
-
-        if 'x-access-token' in request.headers:
-            token = request.headers['x-access-token']
-
-        if not token:
-            return make_response(jsonify({'error': 'Token is missing'}), 403)
-
-        try:
-            data = jwt.decode(token, TOKEN_SECRET_KEY)
-        except jwt.ExpiredSignatureError:
-            return make_response(jsonify({'error': 'Token expired'}), 403)
-        except jwt.InvalidTokenError:
-            return make_response(jsonify({'error': 'Invalid token'}), 403)
-
-        params = dict(kwargs, token_data=data)
-        return fn(*args, **params)
-
-    return decorated
-
-
 @kiskadee.route('/login', methods=['POST'])
 def login():
     """Token based login
@@ -76,8 +38,14 @@ def login():
 
         if user is not None and user.verify_password(password):
             token = user.generate_token()
-            response = {'token': token, 'user': {
-                'id': user.id, 'name': user.name, 'email': user.email}}
+            response = {
+                'token': token,
+                'user': {
+                    'id': user.id,
+                    'name': user.name,
+                    'email': user.email
+                }
+            }
             return make_response(jsonify(response), 200)
 
     return make_response(jsonify({'error': 'Could not verify !'}), 401)
@@ -211,16 +179,20 @@ def create_user():
             email=data.get('email')).first()
 
         if user is not None:
-            return make_response(jsonify({'error': 'user already exists'}), 403)
+            return make_response(
+                jsonify({
+                    'error': 'user already exists'
+                }), 403)
 
     # Try to create user
     try:
         user = UserSchema.create(**data)
     except ValidationError as error:
-        return make_response(jsonify({
-            'error': 'Validation error',
-            'validations': error.args[0]
-        }), 400)
+        return make_response(
+            jsonify({
+                'error': 'Validation error',
+                'validations': error.args[0]
+            }), 400)
 
     db_session.add(user)
     db_session.commit()
@@ -230,6 +202,8 @@ def create_user():
 
     token = user.generate_token()
 
+    mail.send_confirmation_email(user)
+
     return make_response(jsonify({'user': result.data, 'token': token}), 201)
 
 
@@ -276,9 +250,10 @@ def update_user(token_data, user_id):
         return make_response(jsonify({'error': 'user not found'}), 404)
 
     if token_data['user_id'] != user_id:
-        return make_response(jsonify({
-            'error': 'token user does not match to requested user'
-        }), 403)
+        return make_response(
+            jsonify({
+                'error': 'token user does not match to requested user'
+            }), 403)
 
     json_data = request.get_json()
     user_data = UserSchema().dump(user).data
@@ -287,10 +262,11 @@ def update_user(token_data, user_id):
     validation = UserSchema().load(user_data)
 
     if bool(validation.errors):
-        return make_response(jsonify({
-            'error': 'Validation error',
-            'validations': validation.errors
-        }), 400)
+        return make_response(
+            jsonify({
+                'error': 'Validation error',
+                'validations': validation.errors
+            }), 400)
 
     password = validation.data.get('validation')
     if password is not None:
@@ -325,9 +301,10 @@ def delete_user(token_data, user_id):
         return make_response(jsonify({'error': 'user not found'}), 404)
 
     if token_data['user_id'] != user_id:
-        return make_response(jsonify({
-            'error': 'token user does not match to requested user'
-        }), 403)
+        return make_response(
+            jsonify({
+                'error': 'token user does not match to requested user'
+            }), 403)
 
     db_session.delete(user)
     db_session.commit()
diff --git a/kiskadee/api/mail.py b/kiskadee/api/mail.py
index f5bd949..c024034 100644
--- a/kiskadee/api/mail.py
+++ b/kiskadee/api/mail.py
@@ -8,47 +8,67 @@ from flask_mail import Mail, Message
 
 from kiskadee import config
 from kiskadee.api.app import kiskadee
+from kiskadee.api.token import token_vefirication
 
 kiskadee.config.update({
-    'MAIL_ENABLED': config['mail']['MAIL_ENABLED'] == 'True',
-    'MAIL_SERVER': config['mail']['MAIL_SERVER'],
-    'MAIL_PORT': config['mail']['MAIL_PORT'],
-    'MAIL_USERNAME': os.environ.get('MAIL_USERNAME'),
-    'MAIL_PASSWORD': os.environ.get('MAIL_PASSWORD'),
-    'MAIL_USE_TLS': config['mail']['MAIL_USE_TLS'] == 'True',
-    'MAIL_USE_SSL': config['mail']['MAIL_USE_SSL'] == 'True',
-    'EMAIL_TOKEN_SECRET_KEY': os.environ.get('EMAIL_TOKEN_SECRET_KEY', 'dev email token')
+    'MAIL_ENABLED':
+    config['mail']['MAIL_ENABLED'] == 'True',
+    'MAIL_SERVER':
+    config['mail']['MAIL_SERVER'],
+    'MAIL_PORT':
+    config['mail']['MAIL_PORT'],
+    'MAIL_USERNAME':
+    os.environ.get('MAIL_USERNAME'),
+    'MAIL_PASSWORD':
+    os.environ.get('MAIL_PASSWORD'),
+    'MAIL_USE_TLS':
+    config['mail']['MAIL_USE_TLS'] == 'True',
+    'MAIL_USE_SSL':
+    config['mail']['MAIL_USE_SSL'] == 'True',
+    'EMAIL_TOKEN_SECRET_KEY':
+    os.environ.get('EMAIL_TOKEN_SECRET_KEY', 'dev email token')
 })
 
 mail = Mail(kiskadee)
 
-@kiskadee.route('/users/send_email')
-def send_email():
-    token = jwt.encode({
-        'user_id': 123456,
-        'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=60)
-    }, kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
 
-    msg = Message("Sera ?", sender="fabio1079@gmail.com",
-                  recipients=['fabio1079@gmail.com'])
+def send_confirmation_email(user):
+    if not kiskadee.config['MAIL_ENABLED']:
+        return None
+
+    token = jwt.encode(
+        {
+            'user_id': user.id,
+            'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
+        },
+        kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
+
+    msg = Message(
+        "Kiskadee email confirmation",
+        sender=kiskadee.config['MAIL_USERNAME'],
+        recipients=[user.email])
+
     link = url_for("confirm_email", token=token, _external=True)
-    msg.body = 'MAs sera memsmo ?. Link: {}'.format(link)
 
-    mail.send(msg)
+    msg.body = '''
+    Thank you for registering on kiskadee. Now please confirm your e-mail
+    or your account will be deleted from our servers in a few hours.
 
-    return make_response(jsonify({'ok': True}), 200)
+    Confirmation link: {}
+
+    Now if you are receiving this email by mistake, sorry for the
+    inconvenience and please just ignore this message and the account
+    refers to this message will be deleted.
+    '''.format(link)
+
+    mail.send(msg)
 
 
 @kiskadee.route('/users/confirm_email/<token>')
 def confirm_email(token):
-    print("=" * 80)
-    print("Token: {}".format(token))
-    print("=" * 80)
+    vefirication = token_vefirication(token, kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
 
-    try:
-        jwt.decode(token, kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
+    if 'error' in vefirication:
+        return make_response(jsonify(vefirication), 403)
+    else:
         return make_response(jsonify({'ok': True}), 200)
-    except jwt.ExpiredSignatureError:
-        return make_response(jsonify({'error': 'Token expired'}), 403)
-    except jwt.InvalidTokenError:
-        return make_response(jsonify({'error': 'Invalid token'}), 403)
diff --git a/kiskadee/api/token.py b/kiskadee/api/token.py
new file mode 100644
index 0000000..0e4cfcd
--- /dev/null
+++ b/kiskadee/api/token.py
@@ -0,0 +1,63 @@
+import jwt
+import os
+
+from functools import wraps
+
+from flask import request, make_response, jsonify
+
+TOKEN_SECRET_KEY = os.getenv('TOKEN_SECRET_KEY', 'default development key')
+
+
+def token_vefirication(token, verification_key):
+    """
+    Token verification. Given a token and its key it will return the token data
+    or one of the following errors: Token expired, Invalid token.
+
+    returns:
+        {'data': ...}
+        or
+        {'error': 'Token expired'}
+        or
+        {'error': 'Invalid token'}
+    """
+    try:
+        data = jwt.decode(token, verification_key)
+        return {'data': data}
+    except jwt.ExpiredSignatureError:
+        return {'error': 'Token expired'}
+    except jwt.InvalidTokenError:
+        return {'error': 'Invalid token'}
+
+
+def token_required(fn):
+    """Token verification decorator. When applyed on a route it will
+    look for the x-access-token on the request header.
+
+    If it is valid, the the route is executed.
+    Else, the token is missing or is invalid or has expired, either way
+    the user receive a 403 status code when invalid.
+
+    Possible status code:
+        - 403 Forbidden ->
+            "Token is missing" or "Token expired" or "Invalid token"
+    """
+
+    @wraps(fn)
+    def decorated(*args, **kwargs):
+        token = None
+
+        if 'x-access-token' in request.headers:
+            token = request.headers['x-access-token']
+
+        if not token:
+            return make_response(jsonify({'error': 'Token is missing'}), 403)
+
+        vefirication = token_vefirication(token, TOKEN_SECRET_KEY)
+
+        if 'error' in vefirication:
+            return make_response(jsonify(vefirication), 403)
+        else:
+            params = dict(kwargs, token_data=vefirication['data'])
+            return fn(*args, **params)
+
+    return decorated
\ No newline at end of file

From f85e55b8272cbcba030356d65c0aa588ffae4c23 Mon Sep 17 00:00:00 2001
From: Fabio Teixeira <fabio1079@gmail.com>
Date: Dec 22 2017 18:11:04 +0000
Subject: [PATCH 10/10] Add user activation


---

diff --git a/alembic/versions/2dce48cbfea6_added_is_active_to_user_model.py b/alembic/versions/2dce48cbfea6_added_is_active_to_user_model.py
new file mode 100644
index 0000000..f89c379
--- /dev/null
+++ b/alembic/versions/2dce48cbfea6_added_is_active_to_user_model.py
@@ -0,0 +1,30 @@
+"""Added is_active to user model.
+
+Revision ID: 2dce48cbfea6
+Revises: cecf8298b202
+Create Date: 2017-12-11 18:39:46.343369
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2dce48cbfea6'
+down_revision = 'cecf8298b202'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    """Add is_active to user table"""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    """Remove is_active from user table"""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('users', 'is_active')
+    # ### end Alembic commands ###
diff --git a/kiskadee/api/app.py b/kiskadee/api/app.py
index ec83266..5340266 100644
--- a/kiskadee/api/app.py
+++ b/kiskadee/api/app.py
@@ -27,7 +27,8 @@ def login():
 
     Possible status code:
         - 200 Ok -> User token
-        - 401 Unauthorized -> Could not log user
+        - 401 Unauthorized -> Unauthorized, invalid user credentials
+        - 403 Forbidden -> User email not confirmed
     """
     json_data = request.get_json()
     email, password = [json_data.get('email'), json_data.get('password')]
@@ -36,19 +37,16 @@ def login():
         db_session = kiskadee_db_session()
         user = db_session.query(User).filter_by(email=email).first()
 
+        if user is not None and not user.is_active:
+            return make_response(jsonify({'error': 'User email not confirmed'}), 403)
+
         if user is not None and user.verify_password(password):
             token = user.generate_token()
-            response = {
-                'token': token,
-                'user': {
-                    'id': user.id,
-                    'name': user.name,
-                    'email': user.email
-                }
-            }
+
+            response = {'token': token, 'user': UserSchema().dump(user).data}
             return make_response(jsonify(response), 200)
 
-    return make_response(jsonify({'error': 'Could not verify !'}), 401)
+    return make_response(jsonify({'error': 'Unauthorized, invalid user credentials'}), 401)
 
 
 @kiskadee.route('/fetchers')
@@ -196,14 +194,13 @@ def create_user():
 
     db_session.add(user)
     db_session.commit()
+    mail.send_confirmation_email(user)
 
     user_schema = UserSchema()
     result = user_schema.dump(user)
 
     token = user.generate_token()
 
-    mail.send_confirmation_email(user)
-
     return make_response(jsonify({'user': result.data, 'token': token}), 201)
 
 
@@ -319,4 +316,7 @@ def kiskadee_db_session():
 
 def main():
     """Initialize the kiskadee API."""
+    cleaner = mail.UnconfirmedEmailsCleaner()
+    cleaner.start()
+
     kiskadee.run('0.0.0.0')
diff --git a/kiskadee/api/mail.py b/kiskadee/api/mail.py
index c024034..da99a5c 100644
--- a/kiskadee/api/mail.py
+++ b/kiskadee/api/mail.py
@@ -1,12 +1,16 @@
 import os
 import datetime
 import jwt
+
 from threading import Thread
+from time import sleep
 
 from flask import make_response, jsonify, url_for
 from flask_mail import Mail, Message
 
 from kiskadee import config
+from kiskadee.database import Database
+from kiskadee.model import User
 from kiskadee.api.app import kiskadee
 from kiskadee.api.token import token_vefirication
 
@@ -26,22 +30,41 @@ kiskadee.config.update({
     'MAIL_USE_SSL':
     config['mail']['MAIL_USE_SSL'] == 'True',
     'EMAIL_TOKEN_SECRET_KEY':
-    os.environ.get('EMAIL_TOKEN_SECRET_KEY', 'dev email token')
+    os.environ.get('EMAIL_TOKEN_SECRET_KEY', 'dev email token'),
+    'MAIL_CONFIRM_REDIRECT':
+    config['mail']['MAIL_CONFIRM_REDIRECT'],
+    'MAIL_UNCONFIRMED_CLEANER_TIMER':
+    config['mail']['MAIL_UNCONFIRMED_CLEANER_TIMER']
 })
 
 mail = Mail(kiskadee)
 
 
-def send_confirmation_email(user):
-    if not kiskadee.config['MAIL_ENABLED']:
-        return None
-
+def generate_activation_token(user):
+    """
+    Given a user it generates an activation token and returns it
+    """
     token = jwt.encode(
         {
             'user_id': user.id,
             'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24)
         },
-        kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
+        kiskadee.config['EMAIL_TOKEN_SECRET_KEY'],
+        algorithm='HS256')
+
+    return token
+
+
+def send_confirmation_email(user):
+    """
+    Given a user, it check MAIL_ENABLED and if it is True then sends an e-mail
+    to the given user email with a tokenized link to activate the user.
+    """
+    if not kiskadee.config['MAIL_ENABLED']:
+        user.is_active = True
+        return None
+
+    token = generate_activation_token(user)
 
     msg = Message(
         "Kiskadee email confirmation",
@@ -64,11 +87,54 @@ def send_confirmation_email(user):
     mail.send(msg)
 
 
-@kiskadee.route('/users/confirm_email/<token>')
+@kiskadee.route('/users/confirm_email/<token>', methods=['GET'])
 def confirm_email(token):
-    vefirication = token_vefirication(token, kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
+    """
+    Given an activation token generated by mail.send_confirmation_email
+    if its token is valid it activated the token user
+    or returns a validation error
+
+    GET /users/confirm_email/<token>
+
+    Possible status code:
+        - 200 Ok -> E-mail confirmed, go back to kiskadee: <Kiskadee link>
+        - 403 Forbidden -> Token expired or Invalid token
+    """
+    vefirication = token_vefirication(
+        token, kiskadee.config['EMAIL_TOKEN_SECRET_KEY'])
 
     if 'error' in vefirication:
         return make_response(jsonify(vefirication), 403)
-    else:
-        return make_response(jsonify({'ok': True}), 200)
+
+    db_session = Database().session
+
+    user = db_session.query(User).filter_by(
+        id=vefirication['data']['user_id']).first()
+    user.is_active = True
+
+    db_session.commit()
+
+    confirm_message = '''
+    E-mail confirmed, go back to kiskadee: <a href="{}">Kiskadee</a>
+    '''.format(kiskadee.config['MAIL_CONFIRM_REDIRECT'])
+
+    return make_response(confirm_message, 200)
+
+
+class UnconfirmedEmailsCleaner(Thread):
+    """
+    If MAIL_ENABLED is True then for every MAIL_UNCONFIRMED_CLEANER_TIMER time
+    it will search for unactivated users and deletes it.
+    """
+
+    def run(self):
+        if not kiskadee.config['MAIL_ENABLED']:
+            return None
+
+        sleep_time = int(kiskadee.config['MAIL_UNCONFIRMED_CLEANER_TIMER'])
+
+        while True:
+            sleep(sleep_time)
+            db_session = Database().session
+            db_session.query(User).filter_by(is_active=False).delete()
+            db_session.commit()
diff --git a/kiskadee/api/serializers.py b/kiskadee/api/serializers.py
index 44e2f0a..12bbf13 100644
--- a/kiskadee/api/serializers.py
+++ b/kiskadee/api/serializers.py
@@ -101,6 +101,7 @@ class UserSchema(Schema):
                   validate.Length(min=4, max=255)])
     password = fields.Str(load_only=True,
                           validate=validate.Length(min=4, max=255))
+    is_active = fields.Bool(default=False)
 
     def make_object(self, data):
         """Serialize a User object."""
diff --git a/kiskadee/api/token.py b/kiskadee/api/token.py
index 0e4cfcd..ccbd653 100644
--- a/kiskadee/api/token.py
+++ b/kiskadee/api/token.py
@@ -21,7 +21,7 @@ def token_vefirication(token, verification_key):
         {'error': 'Invalid token'}
     """
     try:
-        data = jwt.decode(token, verification_key)
+        data = jwt.decode(token, verification_key, algorithms=['HS256'])
         return {'data': data}
     except jwt.ExpiredSignatureError:
         return {'error': 'Token expired'}
diff --git a/kiskadee/model.py b/kiskadee/model.py
index 4e879ec..795b2b0 100644
--- a/kiskadee/model.py
+++ b/kiskadee/model.py
@@ -2,7 +2,8 @@
 
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy import Column, Integer, UnicodeText, UniqueConstraint,\
-                       Sequence, Unicode, ForeignKey, orm, JSON, String
+                       Sequence, Unicode, ForeignKey, orm, JSON, String,\
+                       Boolean
 from passlib.apps import custom_app_context as pwd_context
 import kiskadee
 
@@ -14,6 +15,7 @@ TOKEN_SECRET_KEY = os.getenv('TOKEN_SECRET_KEY', 'default development key')
 
 Base = declarative_base()
 
+
 class Package(Base):
     """Software packages abstraction.
 
@@ -23,23 +25,21 @@ class Package(Base):
     """
 
     __tablename__ = 'packages'
-    id = Column(Integer,
-                Sequence('packages_id_seq', optional=True), primary_key=True)
+    id = Column(
+        Integer, Sequence('packages_id_seq', optional=True), primary_key=True)
     name = Column(Unicode(255), nullable=False)
     homepage = Column(Unicode(255), nullable=True)
     fetcher_id = Column(Integer, ForeignKey('fetchers.id'), nullable=False)
     versions = orm.relationship('Version', backref='packages')
-    __table_args__ = (
-            UniqueConstraint('name', 'fetcher_id'),
-            )
+    __table_args__ = (UniqueConstraint('name', 'fetcher_id'), )
 
 
 class Fetcher(Base):
     """kiskadee fetcher abstraction."""
 
     __tablename__ = 'fetchers'
-    id = Column(Integer,
-                Sequence('fetchers_id_seq', optional=True), primary_key=True)
+    id = Column(
+        Integer, Sequence('fetchers_id_seq', optional=True), primary_key=True)
     name = Column(Unicode(255), nullable=False, unique=True)
     target = Column(Unicode(255), nullable=True)
     description = Column(UnicodeText)
@@ -50,22 +50,20 @@ class Version(Base):
     """Abstraction of a package version."""
 
     __tablename__ = 'versions'
-    id = Column(Integer,
-                Sequence('versions_id_seq', optional=True), primary_key=True)
+    id = Column(
+        Integer, Sequence('versions_id_seq', optional=True), primary_key=True)
     number = Column(Unicode(100), nullable=False)
     package_id = Column(Integer, ForeignKey('packages.id'), nullable=False)
     analysis = orm.relationship('Analysis', backref='versions')
-    __table_args__ = (
-            UniqueConstraint('number', 'package_id'),
-            )
+    __table_args__ = (UniqueConstraint('number', 'package_id'), )
 
 
 class Analyzer(Base):
     """Abstraction of a static analyzer."""
 
     __tablename__ = 'analyzers'
-    id = Column(Integer,
-                Sequence('analyzers_id_seq', optional=True), primary_key=True)
+    id = Column(
+        Integer, Sequence('analyzers_id_seq', optional=True), primary_key=True)
     name = Column(Unicode(255), nullable=False, unique=True)
     version = Column(Unicode(255), nullable=True)
     analysis = orm.relationship('Analysis', backref='analyzers')
@@ -75,21 +73,21 @@ class Analysis(Base):
     """Abstraction of a package analysis."""
 
     __tablename__ = 'analysis'
-    id = Column(Integer,
-                Sequence('analysis_id_seq', optional=True), primary_key=True)
+    id = Column(
+        Integer, Sequence('analysis_id_seq', optional=True), primary_key=True)
     version_id = Column(Integer, ForeignKey('versions.id'), nullable=False)
     analyzer_id = Column(Integer, ForeignKey('analyzers.id'), nullable=False)
     raw = Column(JSON)
-    report = orm.relationship('Report',
-                              uselist=False, back_populates='analysis')
+    report = orm.relationship(
+        'Report', uselist=False, back_populates='analysis')
 
 
 class Report(Base):
     """Abstraction of a analysis report."""
 
     __tablename__ = 'reports'
-    id = Column(Integer,
-                Sequence('reports_id_seq', optional=True), primary_key=True)
+    id = Column(
+        Integer, Sequence('reports_id_seq', optional=True), primary_key=True)
     analysis_id = Column(Integer, ForeignKey('analysis.id'), nullable=False)
     results = Column(JSON)
     analysis = orm.relationship('Analysis', back_populates='report')
@@ -104,8 +102,8 @@ def create_analyzers(_session):
     """
     list_of_analyzers = dict(kiskadee.config._sections["analyzers"])
     for name, version in list_of_analyzers.items():
-        if not (_session.query(Analyzer).filter(Analyzer.name == name).
-                filter(Analyzer.version == version).first()):
+        if not (_session.query(Analyzer).filter(Analyzer.name == name).filter(
+                Analyzer.version == version).first()):
             new_analyzer = kiskadee.model.Analyzer()
             new_analyzer.name = name
             new_analyzer.version = version
@@ -115,11 +113,12 @@ def create_analyzers(_session):
 
 class User(Base):
     __tablename__ = 'users'
-    id = Column(Integer,
-                Sequence('users_id_seq', optional=True), primary_key=True)
+    id = Column(
+        Integer, Sequence('users_id_seq', optional=True), primary_key=True)
     name = Column(Unicode(255), nullable=False)
     email = Column(String(255), nullable=False, unique=True)
     password_hash = Column(String(128))
+    is_active = Column(Boolean, unique=False, default=False)
 
     def hash_password(self, password):
         """Takes a plain password as argument
@@ -139,9 +138,13 @@ class User(Base):
 
     def generate_token(self):
         """Generates user auth token and returns it"""
-        token = jwt.encode({
-            'user_id': self.id,
-            'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=48)
-        }, TOKEN_SECRET_KEY)
+        token = jwt.encode(
+            {
+                'user_id': self.id,
+                'exp':
+                datetime.datetime.utcnow() + datetime.timedelta(hours=48)
+            },
+            TOKEN_SECRET_KEY,
+            algorithm='HS256')
 
         return token.decode('UTF-8')
diff --git a/kiskadee/tests/test_api_users.py b/kiskadee/tests/test_api_users.py
index d0f0383..051659a 100644
--- a/kiskadee/tests/test_api_users.py
+++ b/kiskadee/tests/test_api_users.py
@@ -9,6 +9,7 @@ import kiskadee.api.app
 from kiskadee.model import User
 from kiskadee.api.serializers import UserSchema
 
+
 def mock_hash_password(self, password):
     """Mock for User model hash_password method.
     It is too slow for the tests.
@@ -17,7 +18,6 @@ def mock_hash_password(self, password):
 
 
 class ApiUsersTestCase(unittest.TestCase):
-
     def setUp(self):
         kiskadee.api.app.kiskadee.testing = True
         self.engine = kiskadee.database.Database('db_test').engine
@@ -30,11 +30,22 @@ class ApiUsersTestCase(unittest.TestCase):
 
     @patch.object(User, 'hash_password', mock_hash_password)
     def _setup_mock_users(self):
-        mock_users_data = [
-            {'name': 'test 1', 'email': 'test@user1.com', 'password': 'test'},
-            {'name': 'test 2', 'email': 'test@user2.com', 'password': 'test'},
-            {'name': 'test 3', 'email': 'test@user3.com', 'password': 'test'}
-        ]
+        mock_users_data = [{
+            'name': 'test 1',
+            'email': 'test@user1.com',
+            'password': 'test',
+            'is_active': True
+        }, {
+            'name': 'test 2',
+            'email': 'test@user2.com',
+            'password': 'test',
+            'is_active': True
+        }, {
+            'name': 'test 3',
+            'email': 'test@user3.com',
+            'password': 'test',
+            'is_active': True
+        }]
 
         for mock_data in mock_users_data:
             user = UserSchema.create(**mock_data)
@@ -53,7 +64,8 @@ class ApiUsersTestCase(unittest.TestCase):
         user_data = {
             'name': 'login',
             'email': 'login@email.com',
-            'password':'login'
+            'password': 'login',
+            'is_active': True
         }
 
         # Creating a user as user.verify_password inside login route
@@ -67,17 +79,49 @@ class ApiUsersTestCase(unittest.TestCase):
             'password': user_data['password']
         }
 
-        response = self.app.post("/login",
-                                 data=json.dumps(login_data),
-                                 content_type='application/json')
+        response = self.app.post(
+            "/login",
+            data=json.dumps(login_data),
+            content_type='application/json')
 
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("token", data)
-        self.assertIn("user_id", data)
-        self.assertEqual(user.id, data['user_id'])
+        self.assertIn("user", data)
+        self.assertEqual(user.id, data['user']['id'])
         self.assertEqual(200, response.status_code)
 
+    # POST /login -> 403 User email not confirmed
+    def test_unconfirmed_user_cant_login(self):
+        kiskadee.api.app.kiskadee_db_session = lambda: self.session
+
+        user_data = {
+            'name': 'login',
+            'email': 'login@email.com',
+            'password': 'login',
+            'is_active': False
+        }
+
+        user = UserSchema.create(**user_data)
+        self.session.add(user)
+        self.session.commit()
+
+        login_data = {
+            'email': user_data['email'],
+            'password': user_data['password']
+        }
+
+        response = self.app.post(
+            "/login",
+            data=json.dumps(login_data),
+            content_type='application/json')
+
+        data = json.loads(response.data.decode("utf-8"))
+
+        self.assertIn("error", data)
+        self.assertEqual("User email not confirmed", data['error'])
+        self.assertEqual(403, response.status_code)
+
     # POST /login -> 401 Unauthorized
     def test_wrong_data_on_user_login_gives_unauthorized_response(self):
         kiskadee.api.app.kiskadee_db_session = lambda: self.session
@@ -85,7 +129,8 @@ class ApiUsersTestCase(unittest.TestCase):
         user_data = {
             'name': 'login',
             'email': 'login@email.com',
-            'password':'login'
+            'password': 'login',
+            'is_active': True
         }
 
         user = UserSchema.create(**user_data)
@@ -97,17 +142,18 @@ class ApiUsersTestCase(unittest.TestCase):
             'password': 'not my password'
         }
 
-        response = self.app.post("/login",
-                                 data=json.dumps(login_data),
-                                 content_type='application/json')
+        response = self.app.post(
+            "/login",
+            data=json.dumps(login_data),
+            content_type='application/json')
 
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("error", data)
-        self.assertEqual(data["error"], "Could not verify !")
+        self.assertEqual(data["error"],
+                         "Unauthorized, invalid user credentials")
         self.assertEqual(401, response.status_code)
 
-
     # GET /users -> 200 ok
     def test_get_users(self):
         def mock_kiskadee_db_session():
@@ -118,8 +164,10 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.get("/users",
-                                headers={'x-access-token': user_token})
+        response = self.app.get(
+            "/users", headers={
+                'x-access-token': user_token
+            })
         data = json.loads(response.data.decode("utf-8"))
         total_users_count = self.session.query(User).count()
 
@@ -132,7 +180,6 @@ class ApiUsersTestCase(unittest.TestCase):
         self.assertIsNone(user.get('password'))
         self.assertIsNone(user.get('password_hash'))
 
-
     # POST /users -> 201 created
     @patch.object(User, 'hash_password', mock_hash_password)
     def test_successful_create_user(self):
@@ -148,16 +195,18 @@ class ApiUsersTestCase(unittest.TestCase):
         }
 
         total_users_before_creation = self.session.query(User).count()
-        response = self.app.post("/users",
-                                 data=json.dumps(new_user_data),
-                                 content_type='application/json')
+        response = self.app.post(
+            "/users",
+            data=json.dumps(new_user_data),
+            content_type='application/json')
         total_users_after_creation = self.session.query(User).count()
 
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("user", data)
         self.assertIn("token", data)
-        self.assertEqual(total_users_after_creation, total_users_before_creation + 1)
+        self.assertEqual(total_users_after_creation,
+                         total_users_before_creation + 1)
         self.assertEqual(data['user']['email'], new_user_data['email'])
         self.assertEqual(201, response.status_code)
 
@@ -168,22 +217,21 @@ class ApiUsersTestCase(unittest.TestCase):
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
-        new_user_data = {
-            'name': 'new user',
-            'password': 'new user'
-        }
+        new_user_data = {'name': 'new user', 'password': 'new user'}
 
         total_users_before_creation = self.session.query(User).count()
-        response = self.app.post("/users",
-                                 data=json.dumps(new_user_data),
-                                 content_type='application/json')
+        response = self.app.post(
+            "/users",
+            data=json.dumps(new_user_data),
+            content_type='application/json')
         total_users_after_creation = self.session.query(User).count()
 
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("error", data)
         self.assertIn("validations", data)
-        self.assertEqual(total_users_after_creation, total_users_before_creation)
+        self.assertEqual(total_users_after_creation,
+                         total_users_before_creation)
         self.assertEqual(data['error'], 'Validation error')
         self.assertEqual(data['validations']['email'][0],
                          'Missing data for required field.')
@@ -203,15 +251,17 @@ class ApiUsersTestCase(unittest.TestCase):
         }
 
         total_users_before_creation = self.session.query(User).count()
-        response = self.app.post("/users",
-                                 data=json.dumps(new_user_data),
-                                 content_type='application/json')
+        response = self.app.post(
+            "/users",
+            data=json.dumps(new_user_data),
+            content_type='application/json')
         total_users_after_creation = self.session.query(User).count()
 
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("error", data)
-        self.assertEqual(total_users_after_creation, total_users_before_creation)
+        self.assertEqual(total_users_after_creation,
+                         total_users_before_creation)
         self.assertEqual(data['error'], 'user already exists')
         self.assertEqual(403, response.status_code)
 
@@ -225,8 +275,10 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.get("/users/%d" % user.id,
-                                headers={'x-access-token': user_token})
+        response = self.app.get(
+            "/users/%d" % user.id, headers={
+                'x-access-token': user_token
+            })
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("user", data)
@@ -247,8 +299,10 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.get("/users/%d" % 123456789,
-                                headers={'x-access-token': user_token})
+        response = self.app.get(
+            "/users/%d" % 123456789, headers={
+                'x-access-token': user_token
+            })
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("error", data)
@@ -266,11 +320,16 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.put("/users/{}".format(user.id),
-                                 data=json.dumps({'email': 'another@email.com',
-                                                  'password': 'password'}),
-                                 content_type='application/json',
-                                 headers={'x-access-token': user_token})
+        response = self.app.put(
+            "/users/{}".format(user.id),
+            data=json.dumps({
+                'email': 'another@email.com',
+                'password': 'password'
+            }),
+            content_type='application/json',
+            headers={
+                'x-access-token': user_token
+            })
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -296,10 +355,15 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.put("/users/{}".format(user.id),
-                                 data=json.dumps({'password_hash': 'ignome_me'}),
-                                 content_type='application/json',
-                                 headers={'x-access-token': user_token})
+        response = self.app.put(
+            "/users/{}".format(user.id),
+            data=json.dumps({
+                'password_hash': 'ignome_me'
+            }),
+            content_type='application/json',
+            headers={
+                'x-access-token': user_token
+            })
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -323,11 +387,16 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.put("/users/{}".format(user.id),
-                                 data=json.dumps({'password': 'foo',
-                                                  'email': 'not an email'}),
-                                 content_type='application/json',
-                                 headers={'x-access-token': user_token})
+        response = self.app.put(
+            "/users/{}".format(user.id),
+            data=json.dumps({
+                'password': 'foo',
+                'email': 'not an email'
+            }),
+            content_type='application/json',
+            headers={
+                'x-access-token': user_token
+            })
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -351,10 +420,13 @@ class ApiUsersTestCase(unittest.TestCase):
             return self.session
 
         def send_request(user, data, token):
-            return self.app.put("/users/{}".format(user.id),
-                                data=json.dumps(data),
-                                content_type='application/json',
-                                headers={'x-access-token': token})
+            return self.app.put(
+                "/users/{}".format(user.id),
+                data=json.dumps(data),
+                content_type='application/json',
+                headers={
+                    'x-access-token': token
+                })
 
         kiskadee.api.app.kiskadee_db_session = mock_kiskadee_db_session
 
@@ -370,7 +442,7 @@ class ApiUsersTestCase(unittest.TestCase):
 
         self.assertIn("error", data)
         self.assertEqual(data['error'],
-            'token user does not match to requested user')
+                         'token user does not match to requested user')
         self.assertEqual(403, response.status_code)
 
         token = user_to_update.generate_token()
@@ -392,10 +464,15 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.put("/users/{}".format(123456789),
-                                 data=json.dumps({'password': 'not found ?'}),
-                                 content_type='application/json',
-                                 headers={'x-access-token': user_token})
+        response = self.app.put(
+            "/users/{}".format(123456789),
+            data=json.dumps({
+                'password': 'not found ?'
+            }),
+            content_type='application/json',
+            headers={
+                'x-access-token': user_token
+            })
 
         data = json.loads(response.data.decode("utf-8"))
 
@@ -413,8 +490,11 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.delete("/users/{}".format(user.id),
-                                    headers={'x-access-token': user_token})
+        response = self.app.delete(
+            "/users/{}".format(user.id),
+            headers={
+                'x-access-token': user_token
+            })
 
         self.assertEqual(response.data, b'')
         self.assertEqual(204, response.status_code)
@@ -435,14 +515,17 @@ class ApiUsersTestCase(unittest.TestCase):
 
         other_user = self.session.query(User).order_by(User.id.desc()).first()
 
-        response = self.app.delete("/users/{}".format(other_user.id),
-                                    headers={'x-access-token': user_token})
+        response = self.app.delete(
+            "/users/{}".format(other_user.id),
+            headers={
+                'x-access-token': user_token
+            })
 
         data = json.loads(response.data.decode("utf-8"))
 
         self.assertIn("error", data)
         self.assertEqual(data['error'],
-            'token user does not match to requested user')
+                         'token user does not match to requested user')
         self.assertEqual(403, response.status_code)
 
     # DELETE /users/:id -> 404 Not Found
@@ -455,8 +538,11 @@ class ApiUsersTestCase(unittest.TestCase):
         user = self.session.query(User).first()
         user_token = user.generate_token()
 
-        response = self.app.delete("/users/{}".format(123456789),
-                                    headers={'x-access-token': user_token})
+        response = self.app.delete(
+            "/users/{}".format(123456789),
+            headers={
+                'x-access-token': user_token
+            })
 
         data = json.loads(response.data.decode("utf-8"))
 
diff --git a/kiskadee/tests/test_model.py b/kiskadee/tests/test_model.py
index 0ea53f0..f531614 100644
--- a/kiskadee/tests/test_model.py
+++ b/kiskadee/tests/test_model.py
@@ -202,7 +202,7 @@ class ModelTestCase(unittest.TestCase):
         u = self.session.query(model.User)\
                 .filter_by(email='test@email.com').first()
         token = u.generate_token()
-        decoded_token = jwt.decode(token, model.TOKEN_SECRET_KEY)
+        decoded_token = jwt.decode(token, model.TOKEN_SECRET_KEY, algorithms=['HS256'])
 
         self.assertGreaterEqual(len(token), 121)
         self.assertEqual(decoded_token['user_id'], u.id)
diff --git a/util/kiskadee.conf b/util/kiskadee.conf
index ff89280..f21b9e0 100644
--- a/util/kiskadee.conf
+++ b/util/kiskadee.conf
@@ -54,8 +54,10 @@ clanganalyzer = 1.0.0
 frama_c = 1.0.0
 
 [mail]
-MAIL_ENABLED = True
+MAIL_ENABLED = False
 MAIL_SERVER = smtp.googlemail.com
 MAIL_PORT = 465
 MAIL_USE_TLS = False
-MAIL_USE_SSL = True
\ No newline at end of file
+MAIL_USE_SSL = True
+MAIL_CONFIRM_REDIRECT = http://localhost:8080/
+MAIL_UNCONFIRMED_CLEANER_TIMER = 3600
\ No newline at end of file

