// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "remoting/protocol/session_authz_authenticator.h"

#include <memory>
#include <utility>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "remoting/base/http_status.h"
#include "remoting/base/logging.h"
#include "remoting/base/protobuf_http_request_config.h"
#include "remoting/base/session_authz_service_client.h"
#include "remoting/proto/session_authz_service.h"
#include "remoting/protocol/authenticator.h"
#include "remoting/protocol/credentials_type.h"
#include "remoting/protocol/session_authz_reauthorizer.h"

namespace remoting::protocol {

namespace {
Authenticator::RejectionReason ToRejectionReason(
    HttpStatus::Code status_code,
    Authenticator::RejectionReason permission_denied_reason) {
  switch (status_code) {
    case HttpStatus::Code::PERMISSION_DENIED:
      return permission_denied_reason;
    case HttpStatus::Code::UNAUTHENTICATED:
      return Authenticator::RejectionReason::INVALID_CREDENTIALS;
    case HttpStatus::Code::RESOURCE_EXHAUSTED:
      return Authenticator::RejectionReason::TOO_MANY_CONNECTIONS;
    case HttpStatus::Code::NETWORK_ERROR:
      return Authenticator::RejectionReason::NETWORK_FAILURE;
    default:
      return Authenticator::RejectionReason::UNEXPECTED_ERROR;
  }
}
}  // namespace

SessionAuthzAuthenticator::SessionAuthzAuthenticator(
    CredentialsType credentials_type,
    std::unique_ptr<SessionAuthzServiceClient> service_client,
    const CreateBaseAuthenticatorCallback& create_base_authenticator_callback)
    : credentials_type_(credentials_type),
      service_client_(std::move(service_client)),
      create_base_authenticator_callback_(create_base_authenticator_callback) {
  DCHECK(credentials_type == CredentialsType::CLOUD_SESSION_AUTHZ ||
         credentials_type == CredentialsType::CORP_SESSION_AUTHZ);
}

SessionAuthzAuthenticator::~SessionAuthzAuthenticator() = default;

void SessionAuthzAuthenticator::Start(base::OnceClosure resume_callback) {
  GenerateHostToken(std::move(resume_callback));
}

CredentialsType SessionAuthzAuthenticator::credentials_type() const {
  return credentials_type_;
}

const Authenticator& SessionAuthzAuthenticator::implementing_authenticator()
    const {
  return *this;
}

Authenticator::State SessionAuthzAuthenticator::state() const {
  switch (session_authz_state_) {
    case SessionAuthzState::NOT_STARTED:
    case SessionAuthzState::WAITING_FOR_SESSION_TOKEN:
      return WAITING_MESSAGE;
    case SessionAuthzState::GENERATING_HOST_TOKEN:
    case SessionAuthzState::VERIFYING_SESSION_TOKEN:
      return PROCESSING_MESSAGE;
    case SessionAuthzState::READY_TO_SEND_HOST_TOKEN:
      return MESSAGE_READY;
    case SessionAuthzState::SHARED_SECRET_FETCHED:
      return underlying_->state();
    case SessionAuthzState::FAILED:
      return REJECTED;
  }
}

bool SessionAuthzAuthenticator::started() const {
  return session_authz_state_ != SessionAuthzState::NOT_STARTED;
}

Authenticator::RejectionReason SessionAuthzAuthenticator::rejection_reason()
    const {
  DCHECK_EQ(state(), REJECTED);

  if (session_authz_state_ == SessionAuthzState::FAILED) {
    return session_authz_rejection_reason_;
  }
  return underlying_->rejection_reason();
}

Authenticator::RejectionDetails SessionAuthzAuthenticator::rejection_details()
    const {
  DCHECK_EQ(state(), REJECTED);

  if (session_authz_state_ == SessionAuthzState::FAILED) {
    return rejection_details_;
  }
  return underlying_->rejection_details();
}

void SessionAuthzAuthenticator::ProcessMessage(
    const JingleAuthentication& message,
    base::OnceClosure resume_callback) {
  DCHECK_EQ(state(), WAITING_MESSAGE);

  switch (session_authz_state_) {
    case SessionAuthzState::WAITING_FOR_SESSION_TOKEN:
      VerifySessionToken(message, std::move(resume_callback));
      break;
    case SessionAuthzState::SHARED_SECRET_FETCHED: {
      DCHECK_EQ(underlying_->state(), WAITING_MESSAGE);
      auto self = weak_factory_.GetWeakPtr();
      underlying_->ProcessMessage(message, std::move(resume_callback));
      if (self) {
        StartReauthorizerIfNecessary();
      }
      break;
    }
    default:
      NOTREACHED() << "Unexpected SessionAuthz state: "
                   << static_cast<int>(session_authz_state_);
  }
}

JingleAuthentication SessionAuthzAuthenticator::GetNextMessage() {
  DCHECK_EQ(state(), MESSAGE_READY);

  JingleAuthentication message;
  auto self = weak_factory_.GetWeakPtr();
  if (underlying_ && underlying_->state() == MESSAGE_READY) {
    message = underlying_->GetNextMessage();
    if (self) {
      StartReauthorizerIfNecessary();
    }
  }
  if (!self) {
    return message;
  }

  if (session_authz_state_ == SessionAuthzState::READY_TO_SEND_HOST_TOKEN) {
    DCHECK(!host_token_.empty());
    message.session_authz_host_token = host_token_;
    session_authz_state_ = SessionAuthzState::WAITING_FOR_SESSION_TOKEN;
  }

  return message;
}

const std::string& SessionAuthzAuthenticator::GetAuthKey() const {
  DCHECK_EQ(state(), ACCEPTED);

  return underlying_->GetAuthKey();
}

const SessionPolicies* SessionAuthzAuthenticator::GetSessionPolicies() const {
  DCHECK_EQ(state(), ACCEPTED);

  return session_policies_.has_value() ? &session_policies_.value() : nullptr;
}

void SessionAuthzAuthenticator::SetReauthorizerForTesting(
    std::unique_ptr<SessionAuthzReauthorizer> reauthorizer) {
  reauthorizer_ = std::move(reauthorizer);
}

void SessionAuthzAuthenticator::SetSessionIdForTesting(
    std::string_view session_id) {
  session_id_ = session_id;
}

void SessionAuthzAuthenticator::SetHostTokenForTesting(
    std::string_view host_token) {
  host_token_ = host_token;
}

void SessionAuthzAuthenticator::GenerateHostToken(
    base::OnceClosure resume_callback) {
  session_authz_state_ = SessionAuthzState::GENERATING_HOST_TOKEN;
  // Safe to use Unretained() for requests made to |service_client_|, since
  // this class owns |service_client_|, which cancels requests once it gets
  // deleted.
  service_client_->GenerateHostToken(
      base::BindOnce(&SessionAuthzAuthenticator::OnHostTokenGenerated,
                     base::Unretained(this), std::move(resume_callback)));
}

void SessionAuthzAuthenticator::OnHostTokenGenerated(
    base::OnceClosure resume_callback,
    const HttpStatus& status,
    std::unique_ptr<internal::GenerateHostTokenResponseStruct> response) {
  if (!status.ok()) {
    HandleSessionAuthzError("GenerateHostToken", status);
    std::move(resume_callback).Run();
    return;
  }
  session_id_ = response->session_id;
  host_token_ = response->host_token;
  session_authz_state_ = SessionAuthzState::READY_TO_SEND_HOST_TOKEN;
  std::move(resume_callback).Run();
}

void SessionAuthzAuthenticator::VerifySessionToken(
    const JingleAuthentication& message,
    base::OnceClosure resume_callback) {
  session_authz_state_ = SessionAuthzState::VERIFYING_SESSION_TOKEN;
  service_client_->VerifySessionToken(
      message.session_authz_session_token,
      base::BindOnce(&SessionAuthzAuthenticator::OnVerifiedSessionToken,
                     base::Unretained(this), message,
                     std::move(resume_callback)));
}

void SessionAuthzAuthenticator::OnVerifiedSessionToken(
    const JingleAuthentication& message,
    base::OnceClosure resume_callback,
    const HttpStatus& status,
    std::unique_ptr<internal::VerifySessionTokenResponseStruct> response) {
  if (!status.ok()) {
    HandleSessionAuthzError("VerifySessionToken", status);
    std::move(resume_callback).Run();
    return;
  }
  if (response->session_id != session_id_) {
    session_authz_state_ = SessionAuthzState::FAILED;
    session_authz_rejection_reason_ = RejectionReason::INVALID_ACCOUNT_ID;
    rejection_details_ = RejectionDetails(base::StringPrintf(
        "Session token verification failed. Expected session ID: %s, actual: "
        "%s",
        session_id_.c_str(), response->session_id.c_str()));
    std::move(resume_callback).Run();
    return;
  }
  session_authz_state_ = SessionAuthzState::SHARED_SECRET_FETCHED;

  // The other side already started the SPAKE authentication.
  underlying_ = create_base_authenticator_callback_.Run(response->shared_secret,
                                                        WAITING_MESSAGE);
  session_policies_ = std::move(response->session_policies);
  verify_token_response_ = std::move(response);
  auto self = weak_factory_.GetWeakPtr();
  underlying_->ProcessMessage(message, std::move(resume_callback));
  if (self) {
    StartReauthorizerIfNecessary();
  }
}

void SessionAuthzAuthenticator::HandleSessionAuthzError(
    const std::string_view& action_name,
    const HttpStatus& status) {
  DCHECK(!status.ok());
  rejection_details_ = RejectionDetails(base::StringPrintf(
      "SessionAuthz %s error, code: %d, message: %s", action_name,
      static_cast<int>(status.error_code()), status.error_message()));
  session_authz_state_ = SessionAuthzState::FAILED;
  session_authz_rejection_reason_ = ToRejectionReason(
      status.error_code(), RejectionReason::AUTHZ_POLICY_CHECK_FAILED);
}

void SessionAuthzAuthenticator::StartReauthorizerIfNecessary() {
  if (reauthorizer_) {
    return;
  }
  if (!underlying_ || underlying_->state() != ACCEPTED) {
    return;
  }
  if (verify_token_response_->session_reauth_token.empty()) {
    // Reauthorization is optional for Cloud hosts but required for Corp.
    if (credentials_type_ == CredentialsType::CLOUD_SESSION_AUTHZ) {
      HOST_LOG << "Reauthorization is not required for this session.";
    } else {
      session_authz_state_ = SessionAuthzState::FAILED;
      session_authz_rejection_reason_ = RejectionReason::UNEXPECTED_ERROR;
      rejection_details_ = RejectionDetails(
          base::StringPrintf("VerifySessionTokenResponse for session id '%s' "
                             "is missing a session reauth token.",
                             session_id_));
      NotifyStateChangeAfterAccepted();
    }
    return;
  }

  reauthorizer_ = std::make_unique<SessionAuthzReauthorizer>(
      service_client_.get(), verify_token_response_->session_id,
      verify_token_response_->session_reauth_token,
      verify_token_response_->session_reauth_token_lifetime,
      base::BindOnce(&SessionAuthzAuthenticator::OnReauthorizationFailed,
                     weak_factory_.GetWeakPtr()));
  reauthorizer_->Start();
  verify_token_response_.reset();
}

void SessionAuthzAuthenticator::OnReauthorizationFailed(
    HttpStatus::Code error_code,
    const Authenticator::RejectionDetails& details) {
  session_authz_state_ = SessionAuthzState::FAILED;
  session_authz_rejection_reason_ = ToRejectionReason(
      error_code, RejectionReason::REAUTHZ_POLICY_CHECK_FAILED);
  rejection_details_ = details;

  reauthorizer_.reset();
  NotifyStateChangeAfterAccepted();
}

}  // namespace remoting::protocol
