// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/mac/auth_session_request.h"
#import <AuthenticationServices/AuthenticationServices.h>
#import <Foundation/Foundation.h>
#include <memory>
#include <string>
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "net/base/apple/url_conversions.h"
#include "url/url_canon.h"
namespace {
// A navigation throttle that calls a closure when a navigation to a specified
// scheme is seen.
class AuthNavigationThrottle : public content::NavigationThrottle {
public:
using SchemeURLFoundCallback = base::OnceCallback<void(const GURL&)>;
AuthNavigationThrottle(content::NavigationHandle* handle,
const std::string& scheme,
SchemeURLFoundCallback scheme_found)
: content::NavigationThrottle(handle),
scheme_(scheme),
scheme_found_(std::move(scheme_found)) {
DCHECK(!scheme_found_.is_null());
}
~AuthNavigationThrottle() override = default;
ThrottleCheckResult WillStartRequest() override { return HandleRequest(); }
ThrottleCheckResult WillRedirectRequest() override { return HandleRequest(); }
const char* GetNameForLogging() override { return "AuthNavigationThrottle"; }
private:
ThrottleCheckResult HandleRequest() {
// Cancel any prerendering.
if (!navigation_handle()->IsInPrimaryMainFrame()) {
DCHECK(navigation_handle()->IsInPrerenderedMainFrame());
return CANCEL_AND_IGNORE;
}
GURL url = navigation_handle()->GetURL();
if (!url.SchemeIs(scheme_))
return PROCEED;
// Paranoia; if the callback was already fired, ignore all further
// navigations that somehow get through before the WebContents deletion
// happens.
if (scheme_found_.is_null())
return CANCEL_AND_IGNORE;
// Post the callback; triggering the deletion of the WebContents that owns
// the navigation that is in the middle of being throttled would likely not
// be the best of ideas.
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(scheme_found_), url));
return CANCEL_AND_IGNORE;
}
// The scheme to watch for.
std::string scheme_;
// The closure to call once the scheme has been seen.
SchemeURLFoundCallback scheme_found_;
};
} // namespace
AuthSessionRequest::~AuthSessionRequest() {
std::string uuid = base::SysNSStringToUTF8(request_.UUID.UUIDString);
auto iter = GetMap().find(uuid);
if (iter == GetMap().end())
return;
GetMap().erase(iter);
}
// static
void AuthSessionRequest::StartNewAuthSession(
ASWebAuthenticationSessionRequest* request,
Profile* profile) {
NSString* error_string = nil;
// Canonicalize the scheme so that it will compare correctly to the GURLs that
// are visited later. Bail if it is invalid.
NSString* raw_scheme = request.callbackURLScheme;
std::optional<std::string> canonical_scheme =
CanonicalizeScheme(base::SysNSStringToUTF8(raw_scheme));
if (!canonical_scheme) {
error_string =
[NSString stringWithFormat:@"Scheme '%@' is not valid as per RFC 3986.",
raw_scheme];
}
// Create a Browser with an empty tab.
Browser* browser = nil;
if (!error_string) {
browser = CreateBrowser(request, profile);
if (!browser) {
error_string = @"Failed to create a WebContents to present the "
@"authorization session.";
}
}
if (error_string) {
// It's not clear what error to return here. -cancelWithError:'s
// documentation says that it has to be an NSError with the domain as
// specified below and a "suitable" ASWebAuthenticationSessionErrorCode, but
// none of those codes really is good for "something went wrong while trying
// to start the authentication session". PresentationContextInvalid will
// have to do.
NSError* error = [NSError
errorWithDomain:ASWebAuthenticationSessionErrorDomain
code:
ASWebAuthenticationSessionErrorCodePresentationContextInvalid
userInfo:@{NSDebugDescriptionErrorKey : error_string}];
[request cancelWithError:error];
return;
}
// Then create the auth session that owns that browser and will intercept
// navigation requests.
content::WebContents* contents =
browser->tab_strip_model()->GetActiveWebContents();
AuthSessionRequest::CreateForWebContents(contents, browser, request,
canonical_scheme.value());
// Only then actually load the requested page, to make sure that if the very
// first navigation is the one that authorizes the login, it's caught.
// https://crbug.com/1195202
contents->GetController().LoadURL(net::GURLWithNSURL(request.URL),
content::Referrer(),
ui::PAGE_TRANSITION_LINK, std::string());
}
// static
void AuthSessionRequest::CancelAuthSession(
ASWebAuthenticationSessionRequest* request) {
std::string uuid = base::SysNSStringToUTF8(request.UUID.UUIDString);
auto iter = GetMap().find(uuid);
if (iter == GetMap().end())
return;
iter->second->CancelAuthSession();
}
// static
std::optional<std::string> AuthSessionRequest::CanonicalizeScheme(
std::string scheme) {
url::RawCanonOutputT<char> canon_output;
url::Component component;
bool result = url::CanonicalizeScheme(
scheme.data(), url::Component(0, static_cast<int>(scheme.size())),
&canon_output, &component);
if (!result)
return std::nullopt;
return std::string(canon_output.data() + component.begin, component.len);
}
std::unique_ptr<content::NavigationThrottle> AuthSessionRequest::CreateThrottle(
content::NavigationHandle* handle) {
// Only attach a throttle to outermost main frames. Note non-primary main
// frames will cancel the navigation in the throttle.
switch (handle->GetNavigatingFrameType()) {
case content::FrameType::kSubframe:
case content::FrameType::kFencedFrameRoot:
return nil;
case content::FrameType::kPrimaryMainFrame:
case content::FrameType::kPrerenderMainFrame:
break;
}
auto scheme_found = base::BindOnce(&AuthSessionRequest::SchemeWasNavigatedTo,
weak_factory_.GetWeakPtr());
return std::make_unique<AuthNavigationThrottle>(handle, scheme_,
std::move(scheme_found));
}
AuthSessionRequest::AuthSessionRequest(
content::WebContents* web_contents,
Browser* browser,
ASWebAuthenticationSessionRequest* request,
std::string scheme)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<AuthSessionRequest>(*web_contents),
browser_(browser),
request_(request),
scheme_(scheme) {
std::string uuid = base::SysNSStringToUTF8(request.UUID.UUIDString);
GetMap()[uuid] = this;
}
// static
Browser* AuthSessionRequest::CreateBrowser(
ASWebAuthenticationSessionRequest* request,
Profile* profile) {
if (!profile)
return nullptr;
bool ephemeral_sessions_allowed_by_policy =
IncognitoModePrefs::GetAvailability(profile->GetPrefs()) !=
policy::IncognitoModeAvailability::kDisabled;
// As per the documentation for `shouldUseEphemeralSession`: "Whether the
// request is honored depends on the user’s default web browser." If policy
// does not allow for the use of an ephemeral session, the options would be
// either to use a non-ephemeral session, or to error out. However, erroring
// out would leave any app that uses `ASWebAuthenticationSession` unable to do
// any sign-in at all via this API. Given that the docs do not actually
// provide a guarantee of an ephemeral session if requested, take advantage of
// that to not block the user's ability to sign in.
if (request.shouldUseEphemeralSession &&
ephemeral_sessions_allowed_by_policy) {
profile = profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
}
if (!profile)
return nullptr;
// Note that this creates a popup-style window to do the signin. This is a
// specific choice motivated by security concerns, and must *not* be changed
// without consultation with the security team.
//
// The UX concern here is that an ordinary tab is not the right tool. This is
// a magical WebContents that will dismiss itself when a valid login happens
// within it, and so an ordinary tab can't be used as it invites a user to
// navigate by putting a new URL or search into the omnibox. The location
// information must be read-only.
//
// But the critical security concern is that the window *must have* a location
// indication. This is an OS API for which UI needs to be created to allow the
// user to log into a website by providing credentials. Chromium must provide
// the user with an indication of where they are using the credentials.
//
// Having a location indicator that is present but read-only is satisfied with
// a popup window. That must not be changed.
//
// Omit it from session restore as well. This is a special window for use by
// this code; if it were restored it would not have the AuthSessionRequest and
// would not behave correctly.
Browser::CreateParams params(Browser::TYPE_POPUP, profile, true);
params.omit_from_session_restore = true;
Browser* browser = Browser::Create(params);
chrome::AddTabAt(browser, GURL("about:blank"), -1, true);
browser->window()->Show();
return browser;
}
// static
AuthSessionRequest::UUIDToSessionRequestMap& AuthSessionRequest::GetMap() {
static base::NoDestructor<UUIDToSessionRequestMap> map;
return *map;
}
void AuthSessionRequest::DestroyWebContents() {
// Detach the WebContents that owns this object from the tab strip. Because
// the Browser is a TYPE_POPUP, there will only be one tab (tab index 0). This
// will cause the browser window to dispose of itself once it realizes that it
// has no tabs left. Close the tab this way (as opposed to, say,
// TabStripModel::CloseWebContentsAt) so that the web page will no longer be
// able to show any dialogs, particularly a `beforeunload` one.
browser_->tab_strip_model()->DetachAndDeleteWebContentsAt(0);
// The destruction of the WebContents triggers a call to
// WebContentsDestroyed() below.
}
void AuthSessionRequest::CancelAuthSession() {
// macOS has requested that this authentication session be canceled. Close the
// browser window and call it a day.
DestroyWebContents();
// `DestroyWebContents` triggered the death of this object; perform no more
// work.
}
void AuthSessionRequest::SchemeWasNavigatedTo(const GURL& url) {
// Notify the OS that the authentication was successful, and provide the URL
// that was navigated to.
[request_ completeWithCallbackURL:net::NSURLWithGURL(url)];
// This is a success, so no cancellation callback is needed.
perform_cancellation_callback_ = false;
// The authentication session is now complete, so close the browser window.
DestroyWebContents();
// `DestroyWebContents` triggered the death of this object; perform no more
// work.
}
void AuthSessionRequest::WebContentsDestroyed() {
// This function can be called through one of three code paths: one of a
// successful login, and two of cancellation.
//
// Success code path:
//
// - The user successfully logged in, in which case the closure of the page
// was triggered above in `SchemeWasNavigatedTo()`.
//
// Cancellation code paths:
//
// - The user closed the window without successfully logging in.
// - The OS asked for cancellation, in which case the closure of the page was
// triggered above in `CancelAuthSession()`.
//
// In both cancellation cases, the OS must receive a cancellation callback.
// (This is an undocumented requirement in the case that the OS asked for the
// cancellation; see https://crbug.com/1400714.)
if (perform_cancellation_callback_) {
NSError* error = [NSError
errorWithDomain:ASWebAuthenticationSessionErrorDomain
code:ASWebAuthenticationSessionErrorCodeCanceledLogin
userInfo:nil];
[request_ cancelWithError:error];
}
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(AuthSessionRequest);
std::unique_ptr<content::NavigationThrottle> MaybeCreateAuthSessionThrottleFor(
content::NavigationHandle* handle) {
AuthSessionRequest* request =
AuthSessionRequest::FromWebContents(handle->GetWebContents());
if (!request)
return nullptr;
return request->CreateThrottle(handle);
}