Commit 21d84ed0 authored by Michael Ershov's avatar Michael Ershov Committed by Commit Bot

Javascript and tests for device attestation during SAML authentication

This CL implements javascript part of code that allows to
perform device attestation during SAML authentication and
adds browser tests for that.

Bug: 1000589
Test: SAMLDeviceAttestationTest.*
Change-Id: I88829456242c59a5d98293188890e4faed03574d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1848374Reviewed-by: default avatarRoman Sorokin [CET] <rsorokin@chromium.org>
Reviewed-by: default avatarMaksim Ivanov <emaxx@chromium.org>
Reviewed-by: default avatarDenis Kuznetsov [CET] <antrim@chromium.org>
Commit-Queue: Michael Ershov <miersh@google.com>
Cr-Commit-Position: refs/heads/master@{#713083}
parent a1a9b81f
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
#include "chrome/browser/chromeos/attestation/mock_tpm_challenge_key.h"
#include <utility>
using ::testing::Invoke;
......
......@@ -344,6 +344,9 @@ cr.define('cr.login', function() {
this.webviewEventManager_.addEventListener(
this.samlHandler_, 'apiPasswordAdded',
this.onSamlApiPasswordAdded_.bind(this));
this.webviewEventManager_.addEventListener(
this.samlHandler_, 'challengeMachineKeyRequired',
this.onChallengeMachineKeyRequired_.bind(this));
this.webviewEventManager_.addEventListener(
this.webview_, 'droplink', this.onDropLink_.bind(this));
......@@ -1056,6 +1059,16 @@ cr.define('cr.login', function() {
}
}
/**
* Invoked when |samlHandler_| fires 'challengeMachineKeyRequired' event.
* @private
*/
onChallengeMachineKeyRequired_(e) {
cr.sendWithPromise(
'samlChallengeMachineKey', e.detail.url, e.detail.challenge)
.then(e.detail.callback);
}
/**
* Invoked when a link is dropped on the webview.
* @private
......@@ -1124,6 +1137,10 @@ cr.define('cr.login', function() {
* @private
*/
onLoadAbort_(e) {
if (this.samlHandler_.isIntentionalAbort()) {
return;
}
this.dispatchEvent(new CustomEvent(
'loadAbort', {detail: {error_code: e.code, src: e.url}}));
}
......
......@@ -36,6 +36,12 @@ cr.define('cr.login', function() {
/** @const */
const SAML_HEADER = 'google-accounts-saml';
/** @const */
const SAML_VERIFIED_ACCESS_CHALLENGE_HEADER = 'x-verified-access-challenge';
/** @const */
const SAML_VERIFIED_ACCESS_RESPONSE_HEADER =
'x-verified-access-challenge-response';
/** @const */
const injectedScriptName = 'samlInjected';
......@@ -80,6 +86,26 @@ cr.define('cr.login', function() {
constructor(webview, startsOnSamlPage) {
super();
/**
* Device attestation flow stages.
* @enum {number}
* @private
*/
SamlHandler.DeviceAttestationStage = {
// No device attestation in progress.
NONE: 1,
// A Redirect was received with a HTTP header that contained a device
// attestation challenge.
CHALLENGE_RECEIVED: 2,
// The Redirect has been canceled and a device attestation challenge
// response is being computed.
ORIGINAL_REDIRECT_CANCELED: 3,
// The device attestation challenge response is available and the
// original Redirect is being followed with the response included in a
// HTTP header.
NAVIGATING_TO_REDIRECT_PAGE: 4,
};
/**
* The webview that serves IdP pages.
* @type {webview}
......@@ -163,6 +189,27 @@ cr.define('cr.login', function() {
*/
this.extractSamlPasswordAttributes = false;
/**
* Current stage of device attestation flow.
* @type {DeviceAttestationStage}
* @private
*/
this.deviceAttestationStage_ = SamlHandler.DeviceAttestationStage.NONE;
/**
* Challenge from IdP to perform device attestation.
* @type {?string}
* @private
*/
this.verifiedAccessChallenge_ = null;
/**
* Response for a device attestation challenge.
* @type {?string}
* @private
*/
this.verifiedAccessChallengeResponse_ = null;
/**
* The password-attributes that were extracted from the SAMLResponse, if
* any. (Doesn't contain the password itself).
......@@ -196,6 +243,18 @@ cr.define('cr.login', function() {
this.webviewEventManager_.addEventListener(
this.webview_, 'loadcommit', this.onLoadCommit_.bind(this));
this.webviewEventManager_.addWebRequestEventListener(
this.webview_.request.onBeforeRequest,
this.onBeforeRequest_.bind(this),
{urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
['blocking']);
this.webviewEventManager_.addWebRequestEventListener(
this.webview_.request.onBeforeSendHeaders,
this.onBeforeSendHeaders_.bind(this),
{urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
['blocking', 'requestHeaders']);
this.webviewEventManager_.addWebRequestEventListener(
this.webview_.request.onHeadersReceived,
this.onHeadersReceived_.bind(this),
......@@ -308,6 +367,10 @@ cr.define('cr.login', function() {
this.pendingIsSamlPage_ = this.startsOnSamlPage_;
this.passwordStore_ = {};
this.deviceAttestationStage_ = SamlHandler.DeviceAttestationStage.NONE;
this.verifiedAccessChallenge_ = null;
this.verifiedAccessChallengeResponse_ = null;
this.apiInitialized_ = false;
this.apiVersion_ = 0;
this.apiToken_ = null;
......@@ -324,6 +387,16 @@ cr.define('cr.login', function() {
return this.getConsolidatedScrapedPasswords_().indexOf(password) >= 0;
}
/**
* Check that last navigation was aborted intentionally. It will be
* continued later, so the abort event can be ignored.
* @return {boolean}
*/
isIntentionalAbort() {
return this.deviceAttestationStage_ ==
SamlHandler.DeviceAttestationStage.ORIGINAL_REDIRECT_CANCELED;
}
/**
* Invoked on the webview's contentload event.
* @private
......@@ -343,6 +416,10 @@ cr.define('cr.login', function() {
* @private
*/
onLoadAbort_(e) {
if (this.isIntentionalAbort()) {
return;
}
if (e.isTopLevel) {
this.abortedTopLevelUrl_ = e.url;
}
......@@ -414,6 +491,126 @@ cr.define('cr.login', function() {
samlPasswordAttributes.readPasswordAttributes(samlResponse);
}
/**
* Receives a response for a device attestation challenge and navigates to
* saved redirect page.
* @param {string} url Url from canceled redirect.
* @param {{success: boolean, response: string}} challengeResponse Response
* for device attestation challenge. If |success| is true, |response|
* contains challenge response. Otherwise |response| contains empty
* string.
* @private
*/
continueDelayedRedirect_(url, challengeResponse) {
if (this.deviceAttestationStage_ !=
SamlHandler.DeviceAttestationStage.ORIGINAL_REDIRECT_CANCELED) {
console.error(
'SamlHandler.continueDelayedRedirect_: incorrect attestation stage');
return;
}
// Save response only if it is successful.
if (challengeResponse.success) {
this.verifiedAccessChallengeResponse_ = challengeResponse.response;
}
// Navigate to the saved destination from the canceled redirect.
this.deviceAttestationStage_ =
SamlHandler.DeviceAttestationStage.NAVIGATING_TO_REDIRECT_PAGE;
this.webview_.src = url;
}
/**
* Invoked before sending a web request. If a challenge for the remote
* attestation was found in a previous request, cancel the current one. It
* will be continued (reinitiated) later when a challenge response is ready.
* @param {Object} details The web-request details.
* @return {BlockingResponse} Allows the event handler to modify network
* requests.
* @private
*/
onBeforeRequest_(details) {
// Default case without Verified Access.
if (this.deviceAttestationStage_ ==
SamlHandler.DeviceAttestationStage.NONE) {
return {};
}
if (this.deviceAttestationStage_ ==
SamlHandler.DeviceAttestationStage.NAVIGATING_TO_REDIRECT_PAGE) {
return {};
}
if ((this.deviceAttestationStage_ ==
SamlHandler.DeviceAttestationStage.CHALLENGE_RECEIVED) &&
(this.verifiedAccessChallenge_ !== null)) {
// Ask backend to compute response for device attestation challenge.
this.dispatchEvent(new CustomEvent('challengeMachineKeyRequired', {
detail: {
url: details.url,
challenge: this.verifiedAccessChallenge_,
callback: this.continueDelayedRedirect_.bind(this, details.url)
}
}));
this.verifiedAccessChallenge_ = null;
// Cancel redirect by changing destination to javascript:void(0).
// That will produce 'loadabort' event that should be ignored.
this.deviceAttestationStage_ =
SamlHandler.DeviceAttestationStage.ORIGINAL_REDIRECT_CANCELED;
return {redirectUrl: 'javascript:void(0)'};
}
// Reset state in case of unexpected requests during device attestation.
this.deviceAttestationStage_ = SamlHandler.DeviceAttestationStage.NONE;
console.error(
'SamlHandler.onBeforeRequest_: incorrect attestation stage');
return {};
}
/**
* Attaches challenge response during device attestation flow.
* @param {Object} details The web-request details.
* @return {BlockingResponse} Allows the event handler to modify network
* requests.
* @private
*/
onBeforeSendHeaders_(details) {
// Default case without Verified Access.
if (this.deviceAttestationStage_ ==
SamlHandler.DeviceAttestationStage.NONE) {
return {};
}
if (this.deviceAttestationStage_ ==
SamlHandler.DeviceAttestationStage.NAVIGATING_TO_REDIRECT_PAGE) {
// Send extra header only if no error was encountered during challenge
// key procedure.
if (this.verifiedAccessChallengeResponse_ === null) {
this.deviceAttestationStage_ =
SamlHandler.DeviceAttestationStage.NONE;
return {};
}
details.requestHeaders.push({
'name': SAML_VERIFIED_ACCESS_RESPONSE_HEADER,
'value': this.verifiedAccessChallengeResponse_
});
this.verifiedAccessChallengeResponse_ = null;
this.deviceAttestationStage_ = SamlHandler.DeviceAttestationStage.NONE;
return {requestHeaders: details.requestHeaders};
}
// Reset state in case of unexpected navigation during device attestation.
this.deviceAttestationStage_ = SamlHandler.DeviceAttestationStage.NONE;
console.error(
'SamlHandler.onBeforeSendHeaders_: incorrect attestation stage');
return {};
}
/**
* Invoked when headers are received for the main frame.
* @private
......@@ -435,6 +632,18 @@ cr.define('cr.login', function() {
this.pendingIsSamlPage_ = false;
}
}
// If true, IdP tries to perform a device attestation.
// 300 <= .. <= 399 means it is a redirect to a page that will verify
// device response. HTTP header with
// |SAML_VERIFIED_ACCESS_CHALLENGE_HEADER| name contains challenge from
// Verified Access Web API.
if ((details.statusCode >= 300) && (details.statusCode <= 399) &&
(headerName == SAML_VERIFIED_ACCESS_CHALLENGE_HEADER)) {
this.deviceAttestationStage_ =
SamlHandler.DeviceAttestationStage.CHALLENGE_RECEIVED;
this.verifiedAccessChallenge_ = header.value;
}
}
return {};
......
......@@ -1322,7 +1322,17 @@ bool GaiaScreenHandler::IsOfflineLoginActive() const {
return (screen_mode_ == GAIA_SCREEN_MODE_OFFLINE) || offline_login_is_active_;
}
void GaiaScreenHandler::SetNextSamlChallengeKeyHandlerForTesting(
std::unique_ptr<SamlChallengeKeyHandler> handler_for_test) {
saml_challenge_key_handler_for_test_ = std::move(handler_for_test);
}
void GaiaScreenHandler::CreateSamlChallengeKeyHandler() {
if (saml_challenge_key_handler_for_test_) {
saml_challenge_key_handler_ =
std::move(saml_challenge_key_handler_for_test_);
return;
}
saml_challenge_key_handler_ = std::make_unique<SamlChallengeKeyHandler>();
}
......
......@@ -134,6 +134,9 @@ class GaiaScreenHandler : public BaseScreenHandler,
// WebUI (i.e. WebUI mignt not have completed transition to the new mode).
bool IsOfflineLoginActive() const;
void SetNextSamlChallengeKeyHandlerForTesting(
std::unique_ptr<SamlChallengeKeyHandler> handler_for_test);
private:
// TODO (xiaoyinh): remove this dependency.
friend class SigninScreenHandler;
......@@ -336,9 +339,8 @@ class GaiaScreenHandler : public BaseScreenHandler,
return !security_token_pin_dialog_closed_callback_.is_null();
}
// Assigns new SamlChallengeKeyHandler object to
// Assigns new SamlChallengeKeyHandler object or an object for testing to
// |saml_challenge_key_handler_|.
// TODO(miersh): ... or assigns an object for testing.
void CreateSamlChallengeKeyHandler();
// Current state of Gaia frame.
......@@ -446,6 +448,7 @@ class GaiaScreenHandler : public BaseScreenHandler,
// Handler for |samlChallengeMachineKey| request.
std::unique_ptr<SamlChallengeKeyHandler> saml_challenge_key_handler_;
std::unique_ptr<SamlChallengeKeyHandler> saml_challenge_key_handler_for_test_;
base::WeakPtrFactory<GaiaScreenHandler> weak_factory_{this};
......
......@@ -4,6 +4,7 @@
#include "chrome/browser/ui/webui/chromeos/login/saml_challenge_key_handler.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/values.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
......@@ -18,6 +19,7 @@ const char kDeviceWebBasedAttestationUrlError[] =
"Device web based attestation is not enabled for the provided URL";
const char kDeviceWebBasedAttestationNotOobeError[] =
"Device web based attestation is only available on the OOBE screen";
const char kChallengeBadBase64Error[] = "Challenge is not base64 encoded.";
const char kSuccessField[] = "success";
const char kResponseField[] = "response";
......@@ -56,12 +58,18 @@ void SamlChallengeKeyHandler::Run(Profile* profile,
DCHECK(!callback_);
callback_ = std::move(callback);
profile_ = profile;
challenge_ = challenge;
// Device attestation is currently allowed only on the OOBE screen.
if (LoginState::Get()->IsUserLoggedIn()) {
ReturnResult(attestation::TpmChallengeKeyResult::MakeError(
kDeviceWebBasedAttestationNotOobeError));
return;
}
if (!base::Base64Decode(challenge, &decoded_challenge_)) {
ReturnResult(attestation::TpmChallengeKeyResult::MakeError(
kChallengeBadBase64Error));
return;
}
BuildResponseForWhitelistedUrl(url);
......@@ -112,7 +120,7 @@ void SamlChallengeKeyHandler::BuildChallengeResponse() {
GetTpmResponseTimeout(), attestation::KEY_DEVICE, profile_,
base::BindOnce(&SamlChallengeKeyHandler::ReturnResult,
weak_factory_.GetWeakPtr()),
challenge_, /*register_key=*/false, /*key_name_for_spkac=*/"");
decoded_challenge_, /*register_key=*/false, /*key_name_for_spkac=*/"");
}
base::TimeDelta SamlChallengeKeyHandler::GetTpmResponseTimeout() const {
......@@ -130,8 +138,11 @@ void SamlChallengeKeyHandler::ReturnResult(
LOG(WARNING) << "Device attestation error: " << result.error_message;
}
std::string encoded_result_data;
base::Base64Encode(result.data, &encoded_result_data);
js_result.SetKey(kSuccessField, base::Value(result.is_success));
js_result.SetKey(kResponseField, base::Value(result.data));
js_result.SetKey(kResponseField, base::Value(encoded_result_data));
std::move(callback_).Run(std::move(js_result));
tpm_key_challenger_.reset();
......
......@@ -49,7 +49,7 @@ class SamlChallengeKeyHandler final {
void ReturnResult(const attestation::TpmChallengeKeyResult& result);
Profile* profile_ = nullptr;
std::string challenge_;
std::string decoded_challenge_;
// Callback to return a result of ChallengeKey.
CallbackType callback_;
......
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