Chromium Code Reviews| Index: chrome/browser/resources/gaia_auth_host/authenticator.js |
| diff --git a/chrome/browser/resources/gaia_auth_host/authenticator.js b/chrome/browser/resources/gaia_auth_host/authenticator.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..aaff9986839a1dcca3c5acf827b1dbfc5253ef64 |
| --- /dev/null |
| +++ b/chrome/browser/resources/gaia_auth_host/authenticator.js |
| @@ -0,0 +1,285 @@ |
| +// 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. After initialization, call {@code load} to start |
| + * the authentication flow. Two events may be raised after this point: |
| + * 1) a 'ready' event when the authentication UI is ready to use. |
| + * 2) a 'completed' event when the authentication is completed successfully. If |
| + * the caller is interested in the user credentials, it may supply a success |
| + * callback when calling {@code load}. The callback will be invoked with the |
| + * available credential data before the 'completed' event is dispatched. |
| + */ |
| + |
| +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} container The webview element or its ID to host IdP |
| + * web pages. |
| + * @constructor |
| + * @extends {cr.EventTarget} |
| + */ |
| + function Authenticator(container) { |
| + this.frame_ = typeof container == 'string' ? $(container) : container; |
| + assert(this.frame_); |
|
Roger Tawa OOO till Jul 10th
2014/10/22 19:30:00
Nit: rename frame_ to webView_ ?
Rename container
guohui
2014/10/23 19:12:01
Done.
|
| + |
| + this.email_ = null; |
| + this.password_ = null; |
| + this.sessionIndex_ = null; |
| + this.chooseWhatToSync_ = false; |
| + this.authFlow_ = AuthFlow.DEFAULT; |
| + this.loaded_ = false; |
| + this.idpOrigin_ = null; |
| + this.continueUrl_ = null; |
| + this.continueUrlWithoutParams_ = null; |
| + this.initialFrameUrl_ = null; |
| + this.reloadUrl_ = null; |
| + |
| + /** |
| + * 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 not present |
| + * authMode: 'x', // Authorization mode, default/offline/desktop. |
| + * } |
| + * } |
| + * </pre> |
| + */ |
| + this.successCallback_ = null; |
| + } |
| + |
| + Authenticator.prototype = Object.create(cr.EventTarget.prototype); |
| + |
| + /** |
| + * Loads the authenticator component with the given parameters. |
| + * @param {AuthMode} authMode Authorization mode. |
| + * @param {Object} data Parameters for the authorization flow. |
| + * @param {function(Object)} successCallback A function to be called when |
| + * the authentication is completed successfully. The callback is |
| + * invoked with a credential object. |
| + */ |
| + Authenticator.prototype.load = function(authMode, data, successCallback) { |
| + 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.successCallback_ = successCallback; |
| + this.authFlow_ = AuthFlow.DEFAULT; |
| + |
| + this.frame_.src = this.reloadUrl_; |
| + this.frame_.addEventListener( |
| + 'newwindow', this.onNewWindow_.bind(this)); |
| + this.frame_.request.onCompleted.addListener( |
| + this.onRequestCompleted_.bind(this), |
| + {urls: ['*://*/*', this.continueUrlWithoutParams_ + '*'], |
| + types: ['main_frame']}, |
| + ['responseHeaders']); |
| + this.frame_.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.frame_.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) { |
| + 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) { |
| + chrome.send('switchToFullTab', [currentUrl]); |
|
xiyuan
2014/10/22 17:20:53
This assumes webui page. Maybe we should have a ca
guohui
2014/10/23 19:12:01
Done.
|
| + return; |
| + } |
| + } |
| + |
| + if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) { |
| + this.frame_.contentWindow.postMessage('', this.frame_.src); |
|
xiyuan
2014/10/22 17:20:53
nit: Maybe using an empty object {} instead of ''?
guohui
2014/10/23 19:12:01
Done.
|
| + } |
| + |
| + if (!this.loaded_) { |
| + this.loaded_ = true; |
| + cr.dispatchSimpleEvent(this, 'ready'); |
| + } |
| + }; |
| + |
| + /** |
| + * 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() { |
| + var skipForNow = false; |
| + if (this.frame_.src.indexOf('ntp=1') >= 0) { |
| + skipForNow = true; |
| + } |
| + |
| + if (!this.email_ && !skipForNow) { |
| + this.frame_.src = this.initialFrameUrl_; |
| + return; |
| + } |
| + |
| + if (this.successCallback_) { |
| + this.successCallback_({email: this.email_, |
| + password: this.password_, |
| + usingSAML: this.authFlow_ == AuthFlow.SAML, |
| + chooseWhatToSync: this.chooseWhatToSync_, |
| + skipForNow: skipForNow, |
| + sessionIndex: this.sessionIndex_ || ''}); |
| + } |
| + cr.dispatchSimpleEvent(this, 'completed'); |
| + }; |
| + |
| + /** |
| + * Invoked when the webview attempts to open a new window. |
| + * @private |
| + */ |
| + Authenticator.prototype.onNewWindow_ = function(e) { |
| + window.open(e.targetUrl, '_blank'); |
|
xiyuan
2014/10/22 17:20:53
I wonder if we should let the embedder to decide w
guohui
2014/10/23 19:12:01
Done.
|
| + e.window.discard(); |
| + }; |
| + |
| + Authenticator.AuthFlow = AuthFlow; |
| + Authenticator.AuthMode = AuthMode; |
| + |
| + return { |
| + // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old |
| + // iframe-based flow is deprecated. |
| + GaiaAuthHost: Authenticator |
| + }; |
| +}); |