blob: 4a1bf5aa7ff4d61688064587da4c1d796283d052 [file] [log] [blame]
// Copyright 2014 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.
/**
* @fileoverview An UI component to authenciate to Chrome. The component hosts
* IdP web pages in a webview. A client who is interested in monitoring
* authentication events should pass a listener object of type
* cr.login.GaiaAuthHost.Listener as defined in this file. After initialization,
* call {@code load} to start the authentication flow.
*/
cr.define('cr.login', function() {
'use strict';
var IDP_ORIGIN = 'https://accounts.google.com/';
var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide';
var CONTINUE_URL =
'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html';
var SIGN_IN_HEADER = 'google-accounts-signin';
var EMBEDDED_FORM_HEADER = 'google-accounts-embedded';
var SAML_HEADER = 'google-accounts-saml';
/**
* The source URL parameter for the constrained signin flow.
*/
var CONSTRAINED_FLOW_SOURCE = 'chrome';
/**
* Enum for the authorization mode, must match AuthMode defined in
* chrome/browser/ui/webui/inline_login_ui.cc.
* @enum {number}
*/
var AuthMode = {
DEFAULT: 0,
OFFLINE: 1,
DESKTOP: 2
};
/**
* Enum for the authorization type.
* @enum {number}
*/
var AuthFlow = {
DEFAULT: 0,
SAML: 1
};
/**
* Initializes the authenticator component.
* @param {webview|string} webview The webview element or its ID to host IdP
* web pages.
* @param {Authenticator.Listener=} opt_listener An optional listener for
* authentication events.
* @constructor
* @extends {cr.EventTarget}
*/
function Authenticator(webview, opt_listener) {
this.webview_ = typeof webview == 'string' ? $(webview) : webview;
assert(this.webview_);
this.listener_ = opt_listener || null;
this.email_ = null;
this.password_ = null;
this.sessionIndex_ = null;
this.chooseWhatToSync_ = false;
this.skipForNow_ = false;
this.authFlow_ = AuthFlow.DEFAULT;
this.loaded_ = false;
this.idpOrigin_ = null;
this.continueUrl_ = null;
this.continueUrlWithoutParams_ = null;
this.initialFrameUrl_ = null;
this.reloadUrl_ = null;
}
// TODO(guohui,xiyuan): no need to inherit EventTarget once we deprecate the
// old event-based signin flow.
Authenticator.prototype = Object.create(cr.EventTarget.prototype);
/**
* An interface for receiving notifications upon authentication events.
* @interface
*/
Authenticator.Listener = function() {};
/**
* Invoked when authentication UI is ready.
*/
Authenticator.Listener.prototype.onReady = function(e) {};
/**
* Invoked when authentication is completed successfully with credential data.
* A credential data object looks like this:
* <pre>
* {@code
* {
* email: 'xx@gmail.com',
* password: 'xxxx', // May be null or empty.
* usingSAML: false,
* chooseWhatToSync: false,
* skipForNow: false,
* sessionIndex: '0'
* }
* }
* </pre>
* @param {Object} credentials A credential data object.
*/
Authenticator.Listener.prototype.onSuccess = function(credentials) {};
/**
* Invoked when the requested URL does not fit the container.
* @param {string} url Request URL.
*/
Authenticator.Listener.prototype.onResize = function(url) {};
/**
* Invoked when a new window event is fired.
* @param {Event} e Event object.
*/
Authenticator.Listener.prototype.onNewWindow = function(e) {};
/**
* Loads the authenticator component with the given parameters.
* @param {AuthMode} authMode Authorization mode.
* @param {Object} data Parameters for the authorization flow.
*/
Authenticator.prototype.load = function(authMode, data) {
this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN;
this.continueUrl_ = data.continueUrl || CONTINUE_URL;
this.continueUrlWithoutParams_ =
this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) ||
this.continueUrl_;
this.isConstrainedWindow_ = data.constrained == '1';
this.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
this.authFlow_ = AuthFlow.DEFAULT;
this.webview_.src = this.reloadUrl_;
this.webview_.addEventListener(
'newwindow', this.onNewWindow_.bind(this));
this.webview_.request.onCompleted.addListener(
this.onRequestCompleted_.bind(this),
{urls: ['*://*/*', this.continueUrlWithoutParams_ + '*'],
types: ['main_frame']},
['responseHeaders']);
this.webview_.request.onHeadersReceived.addListener(
this.onHeadersReceived_.bind(this),
{urls: [this.idpOrigin_ + '*'], types: ['main_frame']},
['responseHeaders']);
window.addEventListener(
'message', this.onMessage_.bind(this), false);
};
/**
* Reloads the authenticator component.
*/
Authenticator.prototype.reload = function() {
this.webview_.src = this.reloadUrl_;
this.authFlow_ = AuthFlow.DEFAULT;
};
Authenticator.prototype.constructInitialFrameUrl_ = function(data) {
var url = this.idpOrigin_ + (data.gaiaPath || IDP_PATH);
url = appendParam(url, 'continue', this.continueUrl_);
url = appendParam(url, 'service', data.service);
if (data.hl)
url = appendParam(url, 'hl', data.hl);
if (data.email)
url = appendParam(url, 'Email', data.email);
if (this.isConstrainedWindow_)
url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
return url;
};
/**
* Invoked when a main frame request in the webview has completed.
* @private
*/
Authenticator.prototype.onRequestCompleted_ = function(details) {
var currentUrl = details.url;
if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
if (currentUrl.indexOf('ntp=1') >= 0) {
this.skipForNow_ = true;
}
this.onAuthCompleted_();
return;
}
if (this.isConstrainedWindow_) {
var isEmbeddedPage = false;
if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
var headers = details.responseHeaders;
for (var i = 0; headers && i < headers.length; ++i) {
if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) {
isEmbeddedPage = true;
break;
}
}
}
if (!isEmbeddedPage && this.listener_) {
this.listener_.onResize(currentUrl);
return;
}
}
if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
this.webview_.contentWindow.postMessage({}, currentUrl);
}
if (!this.loaded_) {
this.loaded_ = true;
if (this.listener_) {
this.listener_.onReady();
}
}
};
/**
* Invoked when headers are received in the main frame of the webview. It
* 1) reads the authenticated user info from a signin header,
* 2) signals the start of a saml flow upon receiving a saml header.
* @return {!Object} Modified request headers.
* @private
*/
Authenticator.prototype.onHeadersReceived_ = function(details) {
var headers = details.responseHeaders;
for (var i = 0; headers && i < headers.length; ++i) {
var header = headers[i];
var headerName = header.name.toLowerCase();
if (headerName == SIGN_IN_HEADER) {
var headerValues = header.value.toLowerCase().split(',');
var signinDetails = {};
headerValues.forEach(function(e) {
var pair = e.split('=');
signinDetails[pair[0].trim()] = pair[1].trim();
});
// Removes "" around.
var email = signinDetails['email'].slice(1, -1);
if (this.email_ != email) {
this.email_ = email;
// Clears the scraped password if the email has changed.
this.password_ = null;
}
this.sessionIndex_ = signinDetails['sessionindex'];
} else if (headerName == SAML_HEADER) {
this.authFlow_ = AuthFlow.SAML;
}
}
};
/**
* Invoked when an HTML5 message is received.
* @param {object} e Payload of the received HTML5 message.
* @private
*/
Authenticator.prototype.onMessage_ = function(e) {
if (e.origin != this.idpOrigin_) {
return;
}
var msg = e.data;
if (msg.method == 'attemptLogin') {
this.email_ = msg.email;
this.password_ = msg.password;
this.chooseWhatToSync_ = msg.chooseWhatToSync;
}
};
/**
* Invoked to process authentication completion.
* @private
*/
Authenticator.prototype.onAuthCompleted_ = function() {
if (!this.listener_) {
return;
}
if (!this.email_ && !this.skipForNow_) {
this.webview_.src = this.initialFrameUrl_;
return;
}
this.listener_.onSuccess({email: this.email_,
password: this.password_,
usingSAML: this.authFlow_ == AuthFlow.SAML,
chooseWhatToSync: this.chooseWhatToSync_,
skipForNow: this.skipForNow_,
sessionIndex: this.sessionIndex_ || ''});
};
/**
* Invoked when the webview attempts to open a new window.
* @private
*/
Authenticator.prototype.onNewWindow_ = function(e) {
if (!this.listener_) {
return;
}
this.listener_.onNewWindow(e);
};
Authenticator.AuthFlow = AuthFlow;
Authenticator.AuthMode = AuthMode;
return {
// TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
// iframe-based flow is deprecated.
GaiaAuthHost: Authenticator
};
});