示例#1
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
示例#2
0
    def password_grant(username: str, password: str):
        """Implements logic for password grant."""

        # check user exists
        user = Users().find_one({"username": username})
        if user is None:
            raise InvalidGrant("Username or password is invalid.")

        # check password is valid
        password_hash = user.pop("password_hash")
        is_valid = check_password_hash(password_hash, password)
        if not is_valid:
            raise InvalidGrant("Username or password is invalid.")

        # generate token
        access_token = LoadedAccessToken(
            user["_id"], user["username"], user.get("scope", {})
        ).encode()
        refresh_token = OAuth2.generate_refresh_token(user["_id"])

        return OAuth2.success_response(access_token, refresh_token)
示例#3
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