Beispiel #1
0
def refresh_token():
    """
    Issue a new set of access and refresh token after validating an old refresh token
    Old refresh token can only be used once and hence is removed from database
    Unused but expired refresh token is also deleted from database
    """

    # get old refresh token from request header
    old_token = request.headers.get("refresh-token")
    if old_token is None:
        raise BadRequest("missing refresh-token")

    # check token exists in database and get expire time and user id
    try:
        old_token_document = RefreshTokens().find_one(
            {"token": UUIDLegacy(UUID(old_token))}, {
                "expire_time": 1,
                "username": 1
            })
        if old_token_document is None:
            raise Unauthorized("refresh-token invalid")
    except Exception:
        raise Unauthorized("refresh-token invalid")

    # check token is not expired
    if old_token_document["expire_time"] < getnow():
        raise Unauthorized("token expired")

    # check user exists
    user = Users().find_one({"username": old_token_document["username"]}, {
        "username": 1,
        "scope": 1
    })
    if user is None:
        raise Unauthorized("user not found")

    # generate token
    access_token = AccessToken.encode(user)
    refresh_token = create_refresh_token(user["username"])
    # delete old refresh token from database
    RefreshTokens().delete_one({"token": UUID(old_token)})

    # send response
    response_json = {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": AccessToken.get_expiry(access_token),
        "refresh_token": refresh_token,
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response
Beispiel #2
0
def token_from_request(request):
    if "token" in request.headers:
        token = request.headers["token"]
    elif "Authorization" in request.headers:
        token = request.headers["Authorization"]
    else:
        token = None
    if token:
        token_parts = token.split(" ")
        if len(token_parts) > 1:
            token = token_parts[1]
    payload = AccessToken.decode(token)
    return AccessToken.Payload(payload)
Beispiel #3
0
def credentials():
    """
    Authorize a user with username and password
    When success, return json object with access and refresh token
    """

    # get username and password from request header
    if "application/x-www-form-urlencoded" in request.content_type:
        username = request.form.get("username")
        password = request.form.get("password")
    else:
        username = request.headers.get("username")
        password = request.headers.get("password")
    if username is None or password is None:
        raise BadRequest("missing username or password")

    # check user exists
    user = Users().find_one({"username": username}, {
        "username": 1,
        "scope": 1,
        "password_hash": 1
    })
    if user is None:
        raise Unauthorized("this user does not exist")

    # check password is valid
    password_hash = user.pop("password_hash")
    is_valid = check_password_hash(password_hash, password)
    if not is_valid:
        raise Unauthorized("password does not match")

    # generate token
    access_token = AccessToken.encode(user)
    access_expires = AccessToken.get_expiry(access_token)
    refresh_token = create_refresh_token(user["username"])

    # send response
    response_json = {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": access_expires,
        "refresh_token": refresh_token,
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response
Beispiel #4
0
def token():
    """
    Issue a new set of access and refresh token after validating an old refresh token
    Old refresh token can only be used once and hence is removed from database
    Unused but expired refresh token is also deleted from database
    """

    # get old refresh token from request header
    old_token = request.headers.get("refresh-token")
    if old_token is None:
        raise BadRequest()

    # check token exists in database and get expire time and user id
    collection = RefreshTokens()
    old_token_document = collection.find_one({"token": UUID(old_token)}, {
        "expire_time": 1,
        "user_id": 1
    })
    if old_token_document is None:
        raise Unauthorized()

    # check token is not expired
    expire_time = old_token_document["expire_time"]
    if expire_time < datetime.now():
        raise Unauthorized("token expired")

    # check user exists
    user_id = old_token_document["user_id"]
    user = Users().find_one({"_id": user_id}, {"password_hash": 0})
    if user is None:
        raise Unauthorized()

    # generate token
    access_token = AccessToken.encode(user)
    refresh_token = uuid4()

    # store refresh token in database
    RefreshTokens().insert_one({
        "token":
        refresh_token,
        "user_id":
        user["_id"],
        "expire_time":
        datetime.now() + timedelta(days=30),
    })

    # delete old refresh token from database
    collection.delete_one({"token": UUID(old_token)})
    collection.delete_many({"expire_time": {"$lte": datetime.now()}})

    # send response
    response_json = {
        "access_token": access_token,
        "refresh_token": refresh_token
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response
Beispiel #5
0
def authorize():
    """
    Authorize a user with username and password
    When success, return json object with access and refresh token
    """

    # get username and password from request header
    if "application/x-www-form-urlencoded" in request.content_type:
        username = request.form.get("username")
        password = request.form.get("password")
    else:
        username = request.headers.get("username")
        password = request.headers.get("password")
    if username is None or password is None:
        raise BadRequest()

    # check user exists
    user = Users().find_one({"username": username})
    if user is None:
        raise Unauthorized("Username incorrect")

    # check password is valid
    password_hash = user.pop("password_hash")
    is_valid = check_password_hash(password_hash, password)
    if not is_valid:
        raise Unauthorized("Password incorrect")

    # check that user is active
    if not user.get("active", False):
        raise Unauthorized("Account is disabled.")

    # generate token
    access_token = AccessToken.encode(user)
    refresh_token = uuid4()

    # store refresh token in database
    RefreshTokens().insert_one({
        "token":
        refresh_token,
        "user_id":
        user["_id"],
        "expire_time":
        datetime.now() + timedelta(days=30),
    })

    # send response
    response_json = {
        "access_token": access_token,
        "refresh_token": refresh_token
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response
Beispiel #6
0
 def wrapper(*args, **kwargs):
     try:
         token = request.headers.get('token', None)
         user = AccessToken.decode(token).get('user', {})
         kwargs['user'] = user
         return f(*args, **kwargs)
     except jwt_exceptions.ExpiredSignatureError:
         raise Unauthorized('token expired')
     except jwt_exceptions.InvalidTokenError:
         raise Unauthorized('token invalid')
     except jwt_exceptions.PyJWTError:
         raise Unauthorized('token invalid')
Beispiel #7
0
 def wrapper(*args, **kwargs):
     try:
         token = request.headers.get("token", None)
         user = AccessToken.decode(token).get("user", {})
         kwargs["user"] = user
         return f(*args, **kwargs)
     except jwt_exceptions.ExpiredSignatureError:
         raise errors.Unauthorized("token expired")
     except jwt_exceptions.InvalidTokenError:
         raise errors.Unauthorized("token invalid")
     except jwt_exceptions.PyJWTError:
         raise errors.Unauthorized("token invalid")
Beispiel #8
0
def tasks():
    """
    List tasks

    [Header] token: access token
    [Body] json
    """

    # check if access token is present
    token = request.headers.get('token')
    if token is None:
        projection = {
            'status': True,
            'timestamp': True,
            'offliner.name': True,
            'offliner.config.mwUrl': True
        }
    else:
        _ = AccessToken.decode(token)
        projection = {'logs': False}

    # setting default limit, offset and sort
    limit = int(request.args.get('limit', 10))
    offset = int(request.args.get('offset', 0))
    sort = 1 if request.args.get('sort', -1) == 1 else -1

    # get tasks from database
    cursor = Tasks().aggregate([
        {
            '$project': projection
        },
        {
            '$sort': {
                'timestamp.creation': sort
            }
        },
        {
            '$skip': offset
        },
        {
            '$limit': limit
        },
    ])
    tasks = [task for task in cursor]

    # send the response
    return jsonify({
        'meta': {
            'limit': limit,
            'offset': offset
        },
        'items': tasks
    })
Beispiel #9
0
def token():
    """
    Issue a new set of access and refresh token after validating an old refresh token
    Old refresh token can only be used once and hence is removed from database
    Unused but expired refresh token is also deleted from database
    """

    # get old refresh token from request header
    old_token = request.headers.get('refresh-token')
    if old_token is None:
        raise BadRequest()

    # check token exists in database and get expire time and user id
    collection = RefreshTokens()
    old_token_document = collection.find_one({'token': UUID(old_token)}, {'expire_time': 1, 'user_id': 1})
    if old_token_document is None:
        raise Unauthorized()

    # check token is not expired
    expire_time = old_token_document['expire_time']
    if expire_time < datetime.now():
        raise Unauthorized('token expired')

    # check user exists
    user_id = old_token_document['user_id']
    user = Users().find_one({'_id': user_id}, {'password_hash': 0})
    if user is None:
        raise Unauthorized()

    # generate token
    access_token = AccessToken.encode(user)
    refresh_token = uuid4()

    # store refresh token in database
    RefreshTokens().insert_one({
        'token': refresh_token,
        'user_id': user['_id'],
        'expire_time': datetime.now() + timedelta(days=30)
    })

    # delete old refresh token from database
    collection.delete_one({'token': UUID(old_token)})
    collection.delete_many({'expire_time': {'$lte': datetime.now()}})

    # send response
    response_json = {
        'access_token': access_token,
        'refresh_token': refresh_token
    }
    response = jsonify(response_json)
    response.headers['Cache-Control'] = 'no-store'
    response.headers['Pragma'] = 'no-cache'
    return response
Beispiel #10
0
def validate():
    """
    Validate an access token
    """
    payload = AccessToken.decode(request.headers.get("access-token"))
    if payload is None:
        raise Unauthorized()

    user = Users().find_one({"username": payload["user"]["username"]})
    if user is None:
        raise Unauthorized()

    return Response()
Beispiel #11
0
def validate():
    """
    Validate an access token
    """
    payload = AccessToken.decode(request.headers.get('access-token'))
    if payload is None:
        raise Unauthorized()

    user = Users().find_one({'username': payload['user']['username']})
    if user is None:
        raise Unauthorized()

    return Response()
Beispiel #12
0
def authorize():
    """
    Authorize a user with username and password
    When success, return json object with access and refresh token
    """

    # get username and password from request header
    if 'application/x-www-form-urlencoded' in request.content_type:
        username = request.form.get('username')
        password = request.form.get('password')
    else:
        username = request.headers.get('username')
        password = request.headers.get('password')
    if username is None or password is None:
        raise BadRequest()

    # check user exists
    user = Users().find_one({'username': username})
    if user is None:
        raise Unauthorized()

    # check password is valid
    password_hash = user.pop('password_hash')
    is_valid = check_password_hash(password_hash, password)
    if not is_valid:
        raise Unauthorized()

    # generate token
    access_token = AccessToken.encode(user)
    refresh_token = uuid4()

    # store refresh token in database
    RefreshTokens().insert_one({
        'token': refresh_token,
        'user_id': user['_id'],
        'expire_time': datetime.now() + timedelta(days=30)
    })

    # send response
    response_json = {
        'access_token': access_token,
        'refresh_token': refresh_token
    }
    response = jsonify(response_json)
    response.headers['Cache-Control'] = 'no-store'
    response.headers['Pragma'] = 'no-cache'
    return response
Beispiel #13
0
def task(id: str):
    """
    Show detail / remove a task

    [Header] token: access token
    """

    # check token exist and is valid
    token = AccessToken.decode(request.headers.get('token'))
    if token is None:
        raise errors.Unauthorized()

    if request.method == 'GET':
        task = Tasks().find_one({'_id': ObjectId(id)})
        return jsonify(task)
    elif request.method == 'DELETE':
        if token.get('scope', {}).get('task', {}).get('delete', False):
            Tasks().delete_one({'_id': ObjectId(id)})
            return Response()
        else:
            raise errors.NotEnoughPrivilege()
Beispiel #14
0
def enqueue_mwoffliner():
    """
    Enqueue mwoffliner tasks

    [Header] token: access token
    [Body] json: array of mwoffliner task configurations
    """
    def validate_task(config: dict):
        schema = {
            'mwUrl': {
                'type': 'string',
                'required': True
            },
            'adminEmail': {
                'type': 'string',
                'required': True
            },
            'verbose': {
                'type': 'boolean',
                'required': False
            }
        }
        validator = Validator(schema, allow_unknown=True)

        if validator.validate(config):
            return None
        else:
            return validator.errors

    def enqueue_task(config: dict):
        document = {
            'status': TaskStatus.PENDING.name,
            'timestamp': {
                'creation': datetime.utcnow(),
                'termination': None
            },
            'offliner': {
                'name': "mwoffliner",
                'config': config
            },
            'logs': []
        }
        validator = Validator(Tasks.schema)
        if not validator.validate(document):
            raise errors.InternalError()

        result = Tasks().insert_one(document)
        task_id = result.inserted_id
        celery.send_task('mwoffliner',
                         task_id=str(task_id),
                         kwargs={'offliner_config': config})

        return task_id

    # check token exist and is valid
    token = AccessToken.decode(request.headers.get('token'))
    if token is None:
        raise errors.Unauthorized()

    # check user can create tasks
    if not token.get('scope', {}).get('task', {}).get('create', False):
        raise errors.NotEnoughPrivilege()

    # check request is a list
    configs = request.get_json()
    if not isinstance(configs, list):
        raise errors.BadRequest('A list of task config is required.')

    # check each item in request is a valid mwoffliner config
    config_error = errors.OfflinerConfigNotValid()
    for index, config in enumerate(configs):
        validation_errors = validate_task(config)
        if validation_errors is not None:
            config_error.errors[index] = validation_errors
    if len(config_error.errors) > 0:
        raise config_error

    # add task to database and enqueue task
    task_ids = []
    for config in configs:
        task_id = enqueue_task(config)
        task_ids.append(task_id)

    # send response
    response = jsonify(task_ids)
    response.status_code = 202
    return response
Beispiel #15
0
def asymmetric_key_auth():
    """authenticate using signed message and generate tokens

    - message in X-SSHAuth-Message HTTP header
    - base64 signature in X-SSHAuth-Signature HTTP header
    - decode standard message: username:timestamp(UTC ISO)
    - verify timestamp is less than a minute old
    - verify username matches our database
    - verify signature of message with username's public keys
    - generate tokens"""

    # check the message's validity
    try:
        message = request.headers["X-SSHAuth-Message"]
        signature = base64.b64decode(request.headers["X-SSHAuth-Signature"])
        username, timestamp = message.split(":", 1)
        timestamp = datetime.datetime.fromisoformat(timestamp)
    except KeyError as exc:
        raise errors.BadRequest("Missing header for `{}`".format("".join(
            exc.args[:1])))
    except binascii.Error:
        raise errors.BadRequest("Invalid signature format (not base64)")
    except Exception as exc:
        logger.error(f"Invalid message format: {exc}")
        logger.exception(exc)
        raise errors.BadRequest("Invalid message format")

    if (datetime.datetime.utcnow() -
            timestamp).total_seconds() > MESSAGE_VALIDITY:
        raise errors.Unauthorized(
            f"message too old or peers desyncrhonised: {MESSAGE_VALIDITY}s")

    user = Users().find_one({"username": username}, {
        "username": 1,
        "scope": 1,
        "ssh_keys": 1
    })
    if user is None:
        raise errors.Unauthorized("User not found")  # we shall never get there

    ssh_keys = user.pop("ssh_keys", [])

    # check that the message was signed with a known private key
    authenticated = False
    with tempfile.TemporaryDirectory() as tmp_dirname:
        tmp_dir = pathlib.Path(tmp_dirname)
        message_path = tmp_dir.joinpath("message")
        signatured_path = tmp_dir.joinpath(f"{message_path.name}.sig")

        with open(message_path, "w", encoding="ASCII") as fp:
            fp.write(message)

        with open(signatured_path, "wb") as fp:
            fp.write(signature)

        for ssh_key in ssh_keys:
            pkcs8_data = ssh_key.get("pkcs8_key")
            if not pkcs8_data:  # User record has no PKCS8 version
                continue

            pkcs8_key = tmp_dir.joinpath("pubkey")
            with open(pkcs8_key, "w") as fp:
                fp.write(pkcs8_data)

            pkey_util = subprocess.run(
                [
                    OPENSSL_BIN,
                    "pkeyutl",
                    "-verify",
                    "-pubin",
                    "-inkey",
                    str(pkcs8_key),
                    "-in",
                    str(message_path),
                    "-sigfile",
                    signatured_path,
                ],
                capture_output=True,
                text=True,
            )
            if pkey_util.returncode == 0:  # signature verified
                authenticated = True
                break
    if not authenticated:
        raise errors.Unauthorized("Could not find matching key for signature")

    # we're now authenticated ; generate tokens
    access_token = AccessToken.encode(user)
    refresh_token = uuid4()

    # store refresh token in database
    RefreshTokens().insert_one({
        "token":
        refresh_token,
        "user_id":
        user["_id"],
        "expire_time":
        getnow() + datetime.timedelta(days=REFRESH_TOKEN_EXPIRY),
    })

    # send response
    response_json = {
        "access_token": access_token,
        "token_type": "bearer",
        "expires_in": datetime.timedelta(hours=TOKEN_EXPIRY).total_seconds(),
        "refresh_token": refresh_token,
    }
    response = jsonify(response_json)
    response.headers["Cache-Control"] = "no-store"
    response.headers["Pragma"] = "no-cache"
    return response