diff --git a/docs/openapi.json b/docs/openapi.json index 3a708640..59fbfdc1 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -7,6 +7,74 @@ }, "parameters": {}, "paths": { + "/api/auth": { + "post": { + "description": "This resource looks for an user with the provided information (email, id) and, if there is such user, creates a JWT cookie on the user browser.", + "operationId": "auth_user", + "parameters": [ + { + "description": "Required. The email of the user.", + "in": "query", + "name": "email", + "required": true, + "type": "string" + }, + { + "description": "Required. API key of the admin.", + "in": "query", + "name": "password", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "User authenticate successfully. Returns the JWT set cookie headers and a successful login boolean.", + "examples": { + "application/json": { + "login": true + } + }, + "schema": { + "properties": { + "login": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "401": { + "description": "Request failed. The data provided could authenticate the user.", + "examples": { + "application/json": { + "message": "Couldn't authenticate." + } + }, + "schema": { + "properties": { + "login": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "500": { + "description": "Request failed. Internal server error.", + "examples": { + "application/json": { + "message": "Internal server error." + } + } + } + }, + "summary": "Authenticates the user with the provided information." + } + }, "/api/ping": { "get": { "description": "Ping the server.", diff --git a/reana_server/config.py b/reana_server/config.py index 849eed54..c946d67f 100644 --- a/reana_server/config.py +++ b/reana_server/config.py @@ -20,3 +20,13 @@ ADMIN_USER_ID = "00000000-0000-0000-0000-000000000000" SHARED_VOLUME_PATH = os.getenv('SHARED_VOLUME_PATH', '/var/reana') + +JWT_SECRET_KEY = 'hyper secret key' + +JWT_TOKEN_LOCATION = ['cookies'] + +JWT_COOKIE_SECURE = False + +JWT_ACCESS_COOKIE_PATH = '/' + +JWT_COOKIE_CSRF_PROTECT = True diff --git a/reana_server/factory.py b/reana_server/factory.py index f93cdec0..2943e4f9 100644 --- a/reana_server/factory.py +++ b/reana_server/factory.py @@ -12,6 +12,7 @@ from flask import Flask from flask_cors import CORS +from flask_jwt_extended import JWTManager from reana_commons.config import REANA_LOG_FORMAT, REANA_LOG_LEVEL from reana_db.database import Session @@ -34,5 +35,6 @@ def create_app(): app.register_blueprint(secrets.blueprint, url_prefix='/api') app.session = Session - CORS(app) + jwt = JWTManager(app) + CORS(app, supports_credentials=True) return app diff --git a/reana_server/rest/users.py b/reana_server/rest/users.py index 1a2c85b1..15e2ba59 100644 --- a/reana_server/rest/users.py +++ b/reana_server/rest/users.py @@ -9,10 +9,14 @@ """Reana-Server User Endpoints.""" import logging +import json import traceback from flask import Blueprint, jsonify, request - +from flask_jwt_extended import ( + create_access_token, create_refresh_token, + set_access_cookies, set_refresh_cookies +) from reana_server.utils import _create_user, _get_users blueprint = Blueprint('users', __name__) @@ -200,3 +204,89 @@ def create_user(): # noqa except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500 + + +@blueprint.route('/auth', methods=['POST']) +def auth_user(): + r"""Endpoint to authenticate users. + + --- + post: + summary: Authenticates the user with the provided information. + description: >- + This resource looks for an user with the provided + information (email, id) and, if there is such user, creates + a JWT cookie on the user browser. + operationId: auth_user + produces: + - application/json + parameters: + - name: email + in: query + description: Required. The email of the user. + required: true + type: string + - name: password + in: query + description: Required. API key of the admin. + required: true + type: string + responses: + 200: + description: >- + User authenticate successfully. Returns the JWT set cookie + headers and a successful login boolean. + schema: + type: object + properties: + login: + type: boolean + examples: + application/json: + { + "login": true + } + 401: + description: >- + Request failed. The data provided could authenticate + the user. + schema: + type: object + properties: + login: + type: boolean + examples: + application/json: + { + "message": "Couldn't authenticate." + } + 500: + description: >- + Request failed. Internal server error. + examples: + application/json: + { + "message": "Internal server error." + } + + """ + try: + data = json.loads(request.data.decode('utf-8')) + user_email = data['username'] + reana_access_token = data['password'] + users = _get_users(None, user_email, None, reana_access_token) + if users: + user = users[0] + access_token = create_access_token(identity=user.id_) + refresh_token = create_refresh_token(identity=user.id_) + + response = jsonify({'login': True}) + set_access_cookies(response, access_token) + set_refresh_cookies(response, refresh_token) + return response, 200 + else: + response = jsonify({'message': "Could not authenticate user."}) + return response, 401 + except Exception as e: + logging.error(traceback.format_exc()) + return jsonify({"message": str(e)}), 500 \ No newline at end of file diff --git a/reana_server/rest/workflows.py b/reana_server/rest/workflows.py index f382b421..5f0912a6 100644 --- a/reana_server/rest/workflows.py +++ b/reana_server/rest/workflows.py @@ -17,6 +17,7 @@ from flask import Blueprint from flask import current_app as app from flask import jsonify, request, send_file +from flask_jwt_extended import jwt_optional, get_jwt_identity from reana_commons.config import INTERACTIVE_SESSION_TYPES from reana_commons.utils import get_workspace_disk_usage from reana_db.database import Session @@ -32,6 +33,7 @@ @blueprint.route('/workflows', methods=['GET']) +@jwt_optional def get_workflows(): # noqa r"""Get all current workflows in REANA. @@ -148,15 +150,16 @@ def get_workflows(): # noqa } """ try: - user = get_user_from_token(request.args.get('access_token')) + user = get_jwt_identity() + if not user: + user = get_user_from_token(request.args.get('access_token')).id_ type = request.args.get('type', 'batch') verbose = request.args.get('verbose', False) response, http_response = current_rwc_api_client.api.\ get_workflows( - user=str(user.id_), + user=str(user), type=type, verbose=bool(verbose)).result() - return jsonify(response), http_response.status_code except HTTPError as e: logging.error(traceback.format_exc()) diff --git a/setup.py b/setup.py index 887d9877..3516bcae 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ 'Flask>=0.11', 'fs>=2.0', 'flask-cors>=3.0.6', + 'flask-jwt-extended>=3.19.0', 'marshmallow>=2.13', 'pyOpenSSL==17.5.0', 'reana-commons[kubernetes]>=0.6.0.dev20190703,<0.7.0', diff --git a/tests/conftest.py b/tests/conftest.py index 96ca0adb..6dd9726a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,7 @@ import pytest from flask import Flask +from flask_jwt_extended import JWTManager from mock import Mock from reana_commons.api_client import BaseAPIClient from reana_db.database import Session @@ -41,7 +42,9 @@ def base_app(): } app = Flask(__name__) app.config.from_mapping(config_mapping) + app.config['JWT_SECRET_KEY'] = "hyper secret key" app.secret_key = "hyper secret key" + JWTManager(app) # Register API routes from reana_server.rest import ping, workflows, users # noqa