Commit 9267a16a authored by Lucas Tenório's avatar Lucas Tenório Committed by Commit Bot

Authenticate Supervision Onboarding pages.

This change adds a webview to the supervision onboarding screen and
adds code that authenticate fetches made by this webview.

The whole flow for displaying a Supervision Onboarding page is:
- Fetch access token scoped to supervision servers.
- Add listeners to webview requests to we can add the access token and
  inspect their HTTP responses. These listeners only fire for requests
  to the URLs that we want.
- Start the request and wait for the webview to finish loading.
- Return all occurrences of the supervision custom HTTP header found in
  the responses.
- Onboarding controller now decides if it should exit the flow or
  continue showing the page based on the custom header values.

Some notes on implementation choices:
- We want to keep most of the logic that controls the flow in the
  Onboarding controller. We plan on reusing this controller to power
  a dedicated WebUI that will provide the same flow outside of the
  OOBE/Login.
- For now, all the server configuration is being done by switches.
  This is necessary since the Supervision server is still being built,
  so the switches make development easier with local dev servers.
  When the server is done these switches will be deleted and become
  constants.
- I had to divide the login:closure_compile target because I needed to
  add the chrome_extensions.js externs file. There's a full explanation
  why that was a problem in the BUILD file.
- Tests are incoming! They are actually ready but I wanted to keep this
  change simple. See the related change if you're curious.

Bug: 958995
Change-Id: Iedb39bc7fb76fb8fdc516171e1e6d094c248561a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1611786Reviewed-by: default avatarOliver Chang <ochang@chromium.org>
Reviewed-by: default avatarMichael Giuffrida <michaelpg@chromium.org>
Commit-Queue: Lucas Tenório <ltenorio@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661057}
parent 0a50243e
......@@ -187,11 +187,6 @@
<!-- Chrome OS Supervised users. -->
<if expr="chromeos">
<include name="IDR_SUPERVISION_ONBOARDING_CONTROLLER_MOJOM_HTML"
file="${root_gen_dir}/chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.html"
use_base_dir="false"
type="BINDATA"
compress="gzip" />
<include name="IDR_SUPERVISION_ONBOARDING_CONTROLLER_MOJOM_LITE_JS"
file="${root_gen_dir}/chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom-lite.js"
use_base_dir="false"
......
......@@ -8,4 +8,8 @@ mojom("mojom") {
sources = [
"onboarding_controller.mojom",
]
public_deps = [
"//url/mojom:url_mojom_gurl",
]
}
......@@ -4,8 +4,10 @@
module chromeos.supervision.mojom;
import "url/mojom/url.mojom";
// Represents user actions that the OnboardingController can handle.
enum OnboardingFlowAction {
enum OnboardingAction {
// The user has expressed intent to skip the remaining screens of the flow.
// When receiving this we will most likely perform cleanup functions and
// order the WebviewHost to exit the flow.
......@@ -16,19 +18,40 @@ enum OnboardingFlowAction {
kShowPreviousPage,
};
struct OnboardingPage {
// Url for the page that needs to be loaded by the webview host.
url.mojom.Url url;
// Only requests to URLs that pass this pattern should be authenticated
// or have their custom headers extracted.
// Documentation on how to write these patterns can be found in:
// https://developer.chrome.com/extensions/match_patterns
string url_filter_pattern;
// Access token used to authenticate the flow page requests. Note that this
// should only be used in requests to URLs that match |url_filter_pattern|.
string access_token;
// Some flow pages are expected to return a custom header in their HTTP
// responses. If this field is set, we will extract the given header from
// responses and return its value when the page fully loads.
// Note that this should only be used in requests to URLs that match
// |url_filter_pattern|.
string? custom_header_name;
};
// Represents a webview host, responsible for displaying supervision
// onboarding pages. This will usually be a WebUI page that contains a
// webview tag and manages its properties.
// TODO(958995): Complete this interface.
interface OnboardingWebviewHost {
// Requests that the webview load the page with the given url.
LoadPage(string url);
// Requests the host to load the given page.
LoadPage(OnboardingPage page) => (string? custom_header_value);
// Requests that the host exit the flow immediately. This might mean
// different things depending on the type of host. If we are running in the
// OOBE we will exit the supervision screen and move the the next OOBE step,
// if we are running in a custom WebUI, we should close it.
// different things depending on the type of host. If we are running in OOBE
// we will exit the supervision screen and move to the next OOBE step, if we
// are running in a custom WebUI, we should close it.
ExitFlow();
};
......@@ -41,5 +64,5 @@ interface OnboardingController {
// Requests the controller to handle the given action.
// The controller will decide the next step to continue/end the flow.
HandleAction(OnboardingFlowAction action);
HandleAction(OnboardingAction action);
};
......@@ -4,12 +4,25 @@
#include "chrome/browser/chromeos/supervision/onboarding_controller_impl.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chromeos/constants/chromeos_switches.h"
#include "services/identity/public/cpp/access_token_fetcher.h"
#include "url/gurl.h"
namespace chromeos {
namespace supervision {
namespace {
// OAuth scope necessary to access the Supervision server.
const char kSupervisionScope[] =
"https://www.googleapis.com/auth/kid.family.readonly";
} // namespace
OnboardingControllerImpl::OnboardingControllerImpl() = default;
OnboardingControllerImpl::~OnboardingControllerImpl() = default;
......@@ -23,27 +36,82 @@ void OnboardingControllerImpl::BindWebviewHost(
mojom::OnboardingWebviewHostPtr webview_host) {
webview_host_ = std::move(webview_host);
std::string start_page_url =
Profile* profile = ProfileManager::GetPrimaryUserProfile();
DCHECK(profile);
identity::IdentityManager* identity_manager =
IdentityManagerFactory::GetForProfile(profile);
std::string account_id = identity_manager->GetPrimaryAccountId();
OAuth2TokenService::ScopeSet scopes{kSupervisionScope};
// base::Unretained is safe here since |access_token_fetcher_| is owned by
// |this|.
access_token_fetcher_ = identity_manager->CreateAccessTokenFetcherForAccount(
account_id, "supervision_onboarding_controller", scopes,
base::BindOnce(&OnboardingControllerImpl::AccessTokenCallback,
base::Unretained(this)),
identity::AccessTokenFetcher::Mode::kImmediate);
}
void OnboardingControllerImpl::HandleAction(mojom::OnboardingAction action) {
DCHECK(webview_host_);
switch (action) {
// TODO(958985): Implement the full flow state machine.
case mojom::OnboardingAction::kSkipFlow:
case mojom::OnboardingAction::kShowNextPage:
case mojom::OnboardingAction::kShowPreviousPage:
webview_host_->ExitFlow();
}
}
void OnboardingControllerImpl::AccessTokenCallback(
GoogleServiceAuthError error,
identity::AccessTokenInfo access_token_info) {
DCHECK(webview_host_);
if (error.state() != GoogleServiceAuthError::NONE) {
webview_host_->ExitFlow();
return;
}
mojom::OnboardingPage page;
page.access_token = access_token_info.token;
page.url_filter_pattern =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
chromeos::switches::kSupervisionOnboardingPageUrlPattern);
page.custom_header_name =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
chromeos::switches::kSupervisionOnboardingStartPageUrl);
if (start_page_url.empty()) {
chromeos::switches::kSupervisionOnboardingHttpResponseHeader);
page.url = GURL(base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
chromeos::switches::kSupervisionOnboardingStartPageUrl));
if (!page.url.is_valid() || page.url_filter_pattern.empty() ||
page.custom_header_name->empty()) {
DVLOG(1) << "Exiting Supervision Onboarding flow since the required flags "
"are unset or invalid.";
webview_host_->ExitFlow();
return;
}
webview_host_->LoadPage(start_page_url);
webview_host_->LoadPage(
page.Clone(), base::BindOnce(&OnboardingControllerImpl::LoadPageCallback,
base::Unretained(this)));
}
void OnboardingControllerImpl::HandleAction(
mojom::OnboardingFlowAction action) {
void OnboardingControllerImpl::LoadPageCallback(
const base::Optional<std::string>& custom_header_value) {
DCHECK(webview_host_);
switch (action) {
// TODO(958985): Implement the full flow state machine.
case mojom::OnboardingFlowAction::kSkipFlow:
case mojom::OnboardingFlowAction::kShowNextPage:
case mojom::OnboardingFlowAction::kShowPreviousPage:
webview_host_->ExitFlow();
return;
std::string expected_header_value =
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
chromeos::switches::kSupervisionOnboardingHttpResponseHeaderValue);
if (!custom_header_value.has_value() ||
!base::EqualsCaseInsensitiveASCII(custom_header_value.value(),
expected_header_value)) {
webview_host_->ExitFlow();
}
}
......
......@@ -5,12 +5,18 @@
#ifndef CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_CONTROLLER_IMPL_H_
#define CHROME_BROWSER_CHROMEOS_SUPERVISION_ONBOARDING_CONTROLLER_IMPL_H_
#include <memory>
#include <string>
#include <vector>
#include "base/macros.h"
#include "base/optional.h"
#include "chrome/browser/chromeos/supervision/mojom/onboarding_controller.mojom.h"
#include "mojo/public/cpp/bindings/binding_set.h"
#include "services/identity/public/cpp/identity_manager.h"
namespace identity {
class AccessTokenFetcher;
}
namespace chromeos {
namespace supervision {
......@@ -25,10 +31,18 @@ class OnboardingControllerImpl : public mojom::OnboardingController {
private:
// mojom::OnboardingController:
void BindWebviewHost(mojom::OnboardingWebviewHostPtr webview_host) override;
void HandleAction(mojom::OnboardingFlowAction action) override;
void HandleAction(mojom::OnboardingAction action) override;
// Callback to get the access token from the IdentityManager.
void AccessTokenCallback(GoogleServiceAuthError error,
identity::AccessTokenInfo access_token_info);
// Callback to OnboardingWebviewHost::LoadPage.
void LoadPageCallback(const base::Optional<std::string>& custom_header_value);
mojom::OnboardingWebviewHostPtr webview_host_;
mojo::BindingSet<mojom::OnboardingController> bindings_;
std::unique_ptr<identity::AccessTokenFetcher> access_token_fetcher_;
DISALLOW_COPY_AND_ASSIGN(OnboardingControllerImpl);
};
......
......@@ -43,6 +43,7 @@ group("closure_compile") {
"internet_detail_dialog:closure_compile",
"kiosk_next_home:closure_compile",
"login:closure_compile",
"login:closure_compile_supervision",
"machine_learning:closure_compile",
"multidevice_setup:closure_compile",
"network_ui:closure_compile",
......
......@@ -47,13 +47,26 @@ js_type_check("closure_compile") {
":recommend_apps",
":saml_confirm_password",
":saml_interstitial",
":supervision_onboarding",
":sync_consent",
":throbber_notice",
":update_required_card",
]
}
# We need to keep the supervision_onboarding compilation separate from the main
# target since they depend on incompatible extern files.
#
# The main compilation target bundles the networking_private.js externs, it
# gets that transitively from its :network_select_login dep.
#
# Supervision needs the chrome_extensions.js extern, but they end up declaring
# the same types, so compilation fails.
js_type_check("closure_compile_supervision") {
deps = [
":supervision_onboarding",
]
}
js2gtest("login_unitjs_tests") {
# These could be unit tests, except they need a browser context in order
# to construct a DOMParser object - so they are webui tests.
......@@ -90,6 +103,7 @@ js_library("fake_oobe") {
deps = [
":oobe_types",
"//ui/login:display_manager_types",
"//ui/webui/resources/js:util",
]
}
......@@ -148,9 +162,15 @@ js_library("multidevice_setup_first_run") {
js_library("supervision_onboarding") {
deps = [
":login_screen_behavior",
"//chrome/browser/chromeos/supervision/mojom:mojom_js_library_for_compile",
"//ui/webui/resources/js:cr",
]
externs_list = [
"$externs_path/chrome_extensions.js",
"$externs_path/webview_tag.js",
]
}
js_library("active_directory_password_change") {
......
......@@ -6,7 +6,9 @@
<link rel="import" href="chrome://oobe/custom_elements.html">
<link rel="import" href="chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.html">
<link rel="import" href="chrome://oobe/supervision_onboarding_controller.mojom.html">
<link rel="import" href="chrome://resources/mojo/url/mojom/url.mojom.html">
<script src="chrome://oobe/supervision/onboarding_controller.mojom-lite.js">
</script>
<!--
UI for the Supervision Onboarding flow that's displayed for the first login
......@@ -15,7 +17,7 @@
<template>
<link rel="stylesheet" href="oobe_flex_layout.css">
<oobe-dialog id="supervision-onboarding-dialog" has-buttons>
<webview id="contentWebview" slot="footer"></webview>
<webview id="contentWebview" slot="footer" hidden></webview>
<div slot="bottom-buttons" class="flex layout horizontal">
<oobe-back-button id="back-button"
on-tap="onBack_">
......
......@@ -3,74 +3,201 @@
// found in the LICENSE file.
/**
* @fileoverview Supervision Onboarding polymer element. It forwards user input
* to the C++ handler.
* @fileoverview Supervision Onboarding polymer element. It loads onboarding
* pages from the web in a webview and forwards user actions to the
* OnboardingController, which contains the full flow state machine.
*/
Polymer({
is: 'supervision-onboarding',
behaviors: [LoginScreenBehavior],
/** Overridden from LoginScreenBehavior. */
EXTERNAL_API: [
'setupMojo',
],
/** @private {?chromeos.supervision.mojom.OnboardingControllerProxy} */
controller_: null,
/**
* @private {?chromeos.supervision.mojom.
* OnboardingWebviewHostCallbackRouter}
*/
hostCallbackRouter_: null,
setupMojo: function() {
this.controller_ =
chromeos.supervision.mojom.OnboardingController.getProxy();
this.hostCallbackRouter_ =
new chromeos.supervision.mojom.OnboardingWebviewHostCallbackRouter();
this.hostCallbackRouter_.loadPage.addListener(pageUrl => {
this.$.contentWebview.src = pageUrl;
});
this.hostCallbackRouter_.exitFlow.addListener(() => {
this.exitFlow_();
});
this.controller_.bindWebviewHost(this.hostCallbackRouter_.createProxy());
},
/** @override */
ready: function() {
this.initializeLoginScreen('SupervisionOnboardingScreen', {
commonScreenSize: true,
resetAllowed: true,
});
},
/** @private */
onBack_: function() {
this.controller_.handleAction(
chromeos.supervision.mojom.OnboardingFlowAction.kShowPreviousPage);
},
/** @private */
onSkip_: function() {
this.controller_.handleAction(
chromeos.supervision.mojom.OnboardingFlowAction.kSkipFlow);
},
/** @private */
onNext_: function() {
this.controller_.handleAction(
chromeos.supervision.mojom.OnboardingFlowAction.kShowNextPage);
},
/** @private */
exitFlow_: function() {
chrome.send('login.SupervisionOnboardingScreen.userActed',
['setup-finished']);
{
class WebviewLoader {
/** @param {!WebView} webview */
constructor(webview) {
this.webview_ = webview;
/**
* Page being currently loaded. If null we are not loading any page yet.
* @private {?chromeos.supervision.mojom.OnboardingPage}
*/
this.page_ = null;
/**
* Custom header values found in responses to requests made by the
* webview.
* @private {?string}
*/
this.customHeaderValue_ = null;
/**
* Pending callback for a webview page load. It will be called with the
* list of custom header values if asked by the controller, or an empty
* array otherwise.
* @private {?function({customHeaderValue: ?string})}
*/
this.pendingLoadPageCallback_ = null;
this.webviewListener_ = this.webviewFinishedLoading_.bind(this);
this.webviewHeaderListener_ = this.onHeadersReceived_.bind(this);
this.webviewAuthListener_ = this.authorizeRequest_.bind(this);
}
/**
* @param {!chromeos.supervision.mojom.OnboardingPage} page
* @return {!Promise<{customHeaderValue: ?string}>}
*/
loadPage(page) {
// TODO(958995): Handle the case where we are still loading the previous
// page but the controller wants to load the next one. For now we just
// resolve the previous callback.
if (this.pendingLoadPageCallback_) {
this.pendingLoadPageCallback_({customHeaderValue: null});
}
this.page_ = page;
this.customHeaderValue_ = null;
this.pendingLoadPageCallback_ = null;
this.webview_.request.onBeforeSendHeaders.addListener(
this.webviewAuthListener_,
{urls: [page.urlFilterPattern], types: ['main_frame']},
['blocking', 'requestHeaders']);
this.webview_.request.onHeadersReceived.addListener(
this.webviewHeaderListener_,
{urls: [page.urlFilterPattern], types: ['main_frame']},
['responseHeaders', 'extraHeaders']);
// TODO(958995): Report load errors through the mojo interface.
// At the moment we are treating any loadstop/abort event as a successful
// load.
this.webview_.addEventListener('loadstop', this.webviewListener_);
this.webview_.addEventListener('loadabort', this.webviewListener_);
this.webview_.src = page.url.url;
return new Promise(resolve => {
this.pendingLoadPageCallback_ = resolve;
});
}
/**
* @param {!Object<{responseHeaders:
* !Array<{name: string, value:string}>}>} responseEvent
* @return {!BlockingResponse}
* @private
*/
onHeadersReceived_(responseEvent) {
if (!this.page_.customHeaderName) {
return {};
}
const header = responseEvent.responseHeaders.find(
h => h.name == this.page_.customHeaderName);
this.customHeaderValue_ = header ? header.value : null;
return {};
}
/**
* Injects headers into the passed request.
*
* @param {!Object} requestEvent
* @return {!BlockingResponse} Modified headers.
* @private
*/
authorizeRequest_(requestEvent) {
requestEvent.requestHeaders.push(
{name: 'Authorization', value: 'Bearer ' + this.page_.accessToken});
return /** @type {!BlockingResponse} */ ({
requestHeaders: requestEvent.requestHeaders,
});
}
/**
* Called when the webview sends a loadstop or loadabort event.
* @private {!Event} e
*/
webviewFinishedLoading_(e) {
this.webview_.request.onBeforeSendHeaders.removeListener(
this.webviewAuthListener_);
this.webview_.request.onHeadersReceived.removeListener(
this.webviewHeaderListener_);
this.webview_.removeEventListener('loadstop', this.webviewListener_);
this.webview_.removeEventListener('loadabort', this.webviewListener_);
this.pendingLoadPageCallback_({
customHeaderValue: this.customHeaderValue_,
});
this.webview_.hidden = false;
}
}
});
Polymer({
is: 'supervision-onboarding',
behaviors: [LoginScreenBehavior],
/** Overridden from LoginScreenBehavior. */
EXTERNAL_API: [
'setupMojo',
],
/** @private {?chromeos.supervision.mojom.OnboardingControllerProxy} */
controller_: null,
/**
* @private {?chromeos.supervision.mojom.
* OnboardingWebviewHostCallbackRouter}
*/
hostCallbackRouter_: null,
/** @private {?WebviewLoader} */
webviewLoader_: null,
setupMojo: function() {
this.webviewLoader_ = new WebviewLoader(this.$.contentWebview);
this.controller_ =
chromeos.supervision.mojom.OnboardingController.getProxy();
this.hostCallbackRouter_ =
new chromeos.supervision.mojom.OnboardingWebviewHostCallbackRouter();
this.hostCallbackRouter_.loadPage.addListener(
this.webviewLoader_.loadPage.bind(this.webviewLoader_));
this.hostCallbackRouter_.exitFlow.addListener(this.exitFlow_.bind(this));
this.controller_.bindWebviewHost(this.hostCallbackRouter_.createProxy());
},
/** @override */
ready: function() {
this.initializeLoginScreen('SupervisionOnboardingScreen', {
commonScreenSize: true,
resetAllowed: true,
});
},
/** @private */
onBack_: function() {
this.controller_.handleAction(
chromeos.supervision.mojom.OnboardingAction.kShowPreviousPage);
},
/** @private */
onSkip_: function() {
this.controller_.handleAction(
chromeos.supervision.mojom.OnboardingAction.kSkipFlow);
},
/** @private */
onNext_: function() {
this.controller_.handleAction(
chromeos.supervision.mojom.OnboardingAction.kShowNextPage);
},
/** @private */
exitFlow_: function() {
chrome.send(
'login.SupervisionOnboardingScreen.userActed', ['setup-finished']);
},
});
}
......@@ -166,9 +166,7 @@ void AddSyncConsentResources(content::WebUIDataSource* source) {
}
void AddSupervisionOnboardingScreenResources(content::WebUIDataSource* source) {
source->AddResourcePath("supervision_onboarding_controller.mojom.html",
IDR_SUPERVISION_ONBOARDING_CONTROLLER_MOJOM_HTML);
source->AddResourcePath("onboarding_controller.mojom-lite.js",
source->AddResourcePath("supervision/onboarding_controller.mojom-lite.js",
IDR_SUPERVISION_ONBOARDING_CONTROLLER_MOJOM_LITE_JS);
}
......
......@@ -483,6 +483,23 @@ const char kShowLoginDevOverlay[] = "show-login-dev-overlay";
const char kSupervisionOnboardingStartPageUrl[] =
"supervision-onboarding-start-page-url";
// Matcher pattern for authenticated requests made by the Supervision
// onboarding.
// TODO(958995): Hardcode this value when the server implementation is ready.
const char kSupervisionOnboardingPageUrlPattern[] =
"supervision-onboarding-page-url-pattern";
// Custom HTTP header expected in responses coming from the supervision server.
// TODO(958995): Hardcode this value when the server implementation is ready.
const char kSupervisionOnboardingHttpResponseHeader[] =
"supervision-onboarding-http-response-header";
// Value expected to be found in custom HTTP header coming from the supervision
// server.
// TODO(958995): Hardcode this value when the server implementation is ready.
const char kSupervisionOnboardingHttpResponseHeaderValue[] =
"supervision-onboarding-http-response-header-value";
// Enables testing for encryption migration UI.
const char kTestEncryptionMigrationUI[] = "test-encryption-migration-ui";
......
......@@ -180,6 +180,12 @@ COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
extern const char kShowAndroidFilesInFilesApp[];
COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
extern const char kSupervisionOnboardingStartPageUrl[];
COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
extern const char kSupervisionOnboardingPageUrlPattern[];
COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
extern const char kSupervisionOnboardingHttpResponseHeader[];
COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
extern const char kSupervisionOnboardingHttpResponseHeaderValue[];
COMPONENT_EXPORT(CHROMEOS_CONSTANTS) extern const char kShowLoginDevOverlay[];
COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
extern const char kTestEncryptionMigrationUI[];
......
......@@ -198,6 +198,9 @@ WebRequestEventInterface.prototype.onBeforeRequest;
/** @type {!WebRequestOptionallySynchronousEvent} */
WebRequestEventInterface.prototype.onBeforeSendHeaders;
/** @type {!WebRequestOptionallySynchronousEvent} */
WebRequestEventInterface.prototype.onHeadersReceived;
/**
* @constructor
* @extends {HTMLIFrameElement}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment