blob: 7366e180a09d17550a07e2c122da3c7e7cb47527 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/webid/federated_auth_request_impl.h"
#include "base/callback.h"
#include "base/strings/string_piece.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/webid/id_token_request_callback_data.h"
#include "content/public/common/content_client.h"
#include "url/url_constants.h"
using blink::mojom::ProvideIdTokenStatus;
using blink::mojom::RequestIdTokenStatus;
namespace content {
namespace {
// Determines whether |host| is same-origin with all of its ancestors in the
// frame tree. Returns false if not.
// |origin| is provided because it is not considered safe to use
// host->GetLastCommittedOrigin() at some times, so FrameServiceBase::origin()
// should be used to obtain the frame's origin.
bool IsSameOriginWithAncestors(RenderFrameHost* host,
const url::Origin& origin) {
RenderFrameHost* parent = host->GetParent();
while (parent) {
if (!parent->GetLastCommittedOrigin().IsSameOriginWith(origin)) {
return false;
}
parent = parent->GetParent();
}
return true;
}
// Checks requirements for URLs received from the IDP.
bool IdpUrlIsValid(const GURL& url) {
if (!url.is_valid() || !url.SchemeIs(url::kHttpsScheme))
return false;
return true;
}
} // namespace
FederatedAuthRequestImpl::FederatedAuthRequestImpl(
RenderFrameHost* host,
mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver)
: FrameServiceBase(host, std::move(receiver)) {}
FederatedAuthRequestImpl::~FederatedAuthRequestImpl() {
// Ensures key data members are destructed in proper order and resolves any
// pending promise.
CompleteRequest(RequestIdTokenStatus::kError, "");
}
// static
void FederatedAuthRequestImpl::Create(
RenderFrameHost* host,
mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) {
DCHECK(host);
// TODO(kenrb): This should also be verified in the renderer process before
// the mojo method is invoked, causing the promise to be rejected.
// https://crbug.com/1141125
// It is safe to access host->GetLastCommittedOrigin during construction
// but FrameServiceBase::origin() should be used thereafter.
if (!IsSameOriginWithAncestors(host, host->GetLastCommittedOrigin())) {
mojo::ReportBadMessage(
"WebID cannot be invoked from within cross-origin iframes.");
return;
}
// FederatedAuthRequestImpl owns itself. It will self-destruct when a mojo
// interface error occurs, the render frame host is deleted, or the render
// frame host navigates to a new document.
new FederatedAuthRequestImpl(host, std::move(receiver));
}
void FederatedAuthRequestImpl::RequestIdToken(const GURL& provider,
const std::string& id_request,
RequestIdTokenCallback callback) {
if (callback_) {
std::move(callback).Run(RequestIdTokenStatus::kErrorTooManyRequests, "");
return;
}
callback_ = std::move(callback);
provider_ = provider;
id_request_ = id_request;
network_manager_ =
IdpNetworkRequestManager::Create(provider, render_frame_host());
if (!network_manager_) {
CompleteRequest(RequestIdTokenStatus::kError, "");
return;
}
request_dialog_controller_ =
GetContentClient()->browser()->CreateIdentityRequestDialogController();
network_manager_->FetchIDPWellKnown(
base::BindOnce(&FederatedAuthRequestImpl::OnWellKnownFetched,
weak_ptr_factory_.GetWeakPtr()));
}
void FederatedAuthRequestImpl::OnWellKnownFetched(
IdpNetworkRequestManager::FetchStatus status,
const std::string& idp_endpoint) {
switch (status) {
case IdpNetworkRequestManager::FetchStatus::kWebIdNotSupported: {
CompleteRequest(RequestIdTokenStatus::kErrorWebIdNotSupportedByProvider,
"");
return;
}
case IdpNetworkRequestManager::FetchStatus::kFetchError: {
CompleteRequest(RequestIdTokenStatus::kErrorFetchingWellKnown, "");
return;
}
case IdpNetworkRequestManager::FetchStatus::kInvalidResponseError: {
CompleteRequest(RequestIdTokenStatus::kErrorInvalidWellKnown, "");
return;
}
case IdpNetworkRequestManager::FetchStatus::kSuccess: {
// Intentional fall-through.
}
}
idp_endpoint_url_ = GURL(base::StringPiece(idp_endpoint));
// TODO(kenrb): Do we have to check that this URL is same-origin with the
// provider, or is that not a requirement?
// https://crbug.com/1141125
if (!IdpUrlIsValid(idp_endpoint_url_)) {
CompleteRequest(RequestIdTokenStatus::kError, "");
return;
}
// Use the web contents of the page that initiated the WebID request (i.e.
// the Relying Party) for showing the initial permission dialog.
WebContents* web_contents =
WebContents::FromRenderFrameHost(render_frame_host());
request_dialog_controller_->ShowInitialPermissionDialog(
web_contents, base::BindOnce(&FederatedAuthRequestImpl::OnSigninApproved,
weak_ptr_factory_.GetWeakPtr()));
}
void FederatedAuthRequestImpl::OnSigninApproved(
IdentityRequestDialogController::UserApproval approval) {
if (approval != IdentityRequestDialogController::UserApproval::kApproved) {
CompleteRequest(RequestIdTokenStatus::kApprovalDeclined, "");
return;
}
network_manager_->SendSigninRequest(
idp_endpoint_url_, id_request_,
base::BindOnce(&FederatedAuthRequestImpl::OnSigninResponseReceived,
weak_ptr_factory_.GetWeakPtr()));
}
void FederatedAuthRequestImpl::OnSigninResponseReceived(
IdpNetworkRequestManager::SigninResponse status,
const std::string& response) {
// |response| is either the URL for the sign-in page or the ID token,
// depending on |status|.
switch (status) {
case IdpNetworkRequestManager::SigninResponse::kLoadIdp: {
GURL idp_signin_page_url = GURL(base::StringPiece(response));
if (!IdpUrlIsValid(idp_signin_page_url)) {
CompleteRequest(RequestIdTokenStatus::kError, "");
return;
}
WebContents* rp_web_contents =
WebContents::FromRenderFrameHost(render_frame_host());
DCHECK(!idp_web_contents_);
idp_web_contents_ = CreateIdpWebContents();
request_dialog_controller_->ShowIdProviderWindow(
rp_web_contents, idp_web_contents_.get(), idp_signin_page_url,
base::BindOnce(&FederatedAuthRequestImpl::OnIdpPageClosed,
weak_ptr_factory_.GetWeakPtr()));
return;
}
case IdpNetworkRequestManager::SigninResponse::kTokenGranted: {
// TODO(kenrb): Returning success here has to be dependent on whether
// a WebID flow has succeeded in the past, otherwise jump to
// the token permission dialog.
CompleteRequest(RequestIdTokenStatus::kSuccess, response);
return;
}
case IdpNetworkRequestManager::SigninResponse::kSigninError: {
CompleteRequest(RequestIdTokenStatus::kErrorFetchingSignin, "");
return;
}
case IdpNetworkRequestManager::SigninResponse::kInvalidResponseError: {
CompleteRequest(RequestIdTokenStatus::kErrorInvalidSigninResponse, "");
return;
}
}
}
void FederatedAuthRequestImpl::OnTokenProvided(const std::string& id_token) {
id_token_ = id_token;
// Close the IDP window which leads to OnIdpPageClosed which is our common.
//
// TODO(majidvp): Consider if we should not wait on the IDP window closing and
// instead should directly call `OnIdpPageClosed` here.
request_dialog_controller_->CloseIdProviderWindow();
// Note that we always process the token on `OnIdpPageClosed()`.
// It is possible to get there either via:
// (a) IDP providing a token as shown below, or
// (b) User closing the sign-in window.
//
// +-----------------------+ +-------------------+ +-----------------+
// | FederatedAuthRequest | | DialogController | | IDPWebContents |
// +-----------------------+ +-------------------+ +-----------------+
// | | |
// | ShowIdProviderWindow() | |
// |-------------------------->| |
// | | |
// | | navigate to idp.com |
// | |----------------------->|
// | | |
// | | OnTokenProvided(token)|
// |<---------------------------------------------------|
// | | |
// | CloseIdProviderWindow() | |
// |-------------------------->| |
// | | |
// | closed | |
// |<--------------------------| |
// | | |
// OnIdpPageClosed() | |
// | | |
//
}
void FederatedAuthRequestImpl::OnIdpPageClosed() {
// This could happen if provider didn't provide any token or user closed the
// IdP window before it could.
if (id_token_.empty()) {
CompleteRequest(RequestIdTokenStatus::kError, "");
return;
}
request_dialog_controller_->ShowTokenExchangePermissionDialog(
base::BindOnce(&FederatedAuthRequestImpl::OnTokenProvisionApproved,
weak_ptr_factory_.GetWeakPtr()));
}
void FederatedAuthRequestImpl::OnTokenProvisionApproved(
IdentityRequestDialogController::UserApproval approval) {
if (approval != IdentityRequestDialogController::UserApproval::kApproved) {
CompleteRequest(RequestIdTokenStatus::kApprovalDeclined, "");
return;
}
CompleteRequest(RequestIdTokenStatus::kSuccess, id_token_);
}
std::unique_ptr<WebContents> FederatedAuthRequestImpl::CreateIdpWebContents() {
auto idp_web_contents = content::WebContents::Create(
WebContents::CreateParams(render_frame_host()->GetBrowserContext()));
// Store the callback on the provider web contents so that it can be
// used later.
IdTokenRequestCallbackData::Set(
idp_web_contents.get(),
base::BindOnce(&FederatedAuthRequestImpl::OnTokenProvided,
weak_ptr_factory_.GetWeakPtr()));
return idp_web_contents;
}
void FederatedAuthRequestImpl::CompleteRequest(
blink::mojom::RequestIdTokenStatus status,
const std::string& id_token) {
request_dialog_controller_.reset();
network_manager_.reset();
// Given that |request_dialog_controller_| has reference to this web content
// instance we destroy that first.
idp_web_contents_.reset();
if (callback_)
std::move(callback_).Run(status, id_token);
}
// ---- Provider logic -----
void FederatedAuthRequestImpl::ProvideIdToken(
const std::string& id_token,
ProvideIdTokenCallback idp_callback) {
// The ptr below is actually the same as |idp_web_contents_| but because this
// is a different instance of |FederatedAuthRequestImpl| for which
// |idp_web_contents_| has not been initialized.
//
// TODO(majidvp): We should have two separate mojo service for request and
// response sides would have make this more obvious. http://crbug.com/1141125
WebContents* idp_web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host());
auto* request_callback_data =
IdTokenRequestCallbackData::Get(idp_web_contents);
// TODO(majidvp): This may happen if the page is not loaded by the browser's
// WebID machinery. We need a way for IDP logic to detect that and not provide
// a token. The current plan is to send a special header but we may also need
// to not expose this in JS somehow. Investigate this further.
// http://crbug.com/1141125
if (!request_callback_data) {
std::move(idp_callback).Run(ProvideIdTokenStatus::kError);
return;
}
// After running the RP done callback the IDP sign-in page gets closed and its
// web contents cleared in `FederatedAuthRequestImpl::CompleteRequest()`. So
// we should not access |idp_web_contents| or any of its associated objects
// as it may already be destructed. This is why we first run any logic that
// needs to touch the IDP web contents and then run the RP done callback.
auto rp_done_callback = request_callback_data->TakeDoneCallback();
IdTokenRequestCallbackData::Remove(idp_web_contents);
if (!rp_done_callback) {
std::move(idp_callback).Run(ProvideIdTokenStatus::kErrorTooManyResponses);
return;
}
std::move(idp_callback).Run(ProvideIdTokenStatus::kSuccess);
std::move(rp_done_callback).Run(id_token);
// Don't access |idp_web_contents| passed this point.
}
} // namespace content