Home | History | Annotate | Download | only in gaia_auth
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview
      7  * Script to be injected into SAML provider pages, serving three main purposes:
      8  * 1. Signal hosting extension that an external page is loaded so that the
      9  *    UI around it should be changed accordingly;
     10  * 2. Provide an API via which the SAML provider can pass user credentials to
     11  *    Chrome OS, allowing the password to be used for encrypting user data and
     12  *    offline login.
     13  * 3. Scrape password fields, making the password available to Chrome OS even if
     14  *    the SAML provider does not support the credential passing API.
     15  */
     16 
     17 (function() {
     18   function APICallForwarder() {
     19   }
     20 
     21   /**
     22    * The credential passing API is used by sending messages to the SAML page's
     23    * |window| object. This class forwards API calls from the SAML page to a
     24    * background script and API responses from the background script to the SAML
     25    * page. Communication with the background script occurs via a |Channel|.
     26    */
     27   APICallForwarder.prototype = {
     28     // Channel to which API calls are forwarded.
     29     channel_: null,
     30 
     31     /**
     32      * Initialize the API call forwarder.
     33      * @param {!Object} channel Channel to which API calls should be forwarded.
     34      */
     35     init: function(channel) {
     36       this.channel_ = channel;
     37       this.channel_.registerMessage('apiResponse',
     38                                     this.onAPIResponse_.bind(this));
     39 
     40       window.addEventListener('message', this.onMessage_.bind(this));
     41     },
     42 
     43     onMessage_: function(event) {
     44       if (event.source != window ||
     45           typeof event.data != 'object' ||
     46           !event.data.hasOwnProperty('type') ||
     47           event.data.type != 'gaia_saml_api') {
     48         return;
     49       }
     50       // Forward API calls to the background script.
     51       this.channel_.send({name: 'apiCall', call: event.data.call});
     52     },
     53 
     54     onAPIResponse_: function(msg) {
     55       // Forward API responses to the SAML page.
     56       window.postMessage({type: 'gaia_saml_api_reply', response: msg.response},
     57                          '/');
     58     }
     59   };
     60 
     61   /**
     62    * A class to scrape password from type=password input elements under a given
     63    * docRoot and send them back via a Channel.
     64    */
     65   function PasswordInputScraper() {
     66   }
     67 
     68   PasswordInputScraper.prototype = {
     69     // URL of the page.
     70     pageURL_: null,
     71 
     72     // Channel to send back changed password.
     73     channel_: null,
     74 
     75     // An array to hold password fields.
     76     passwordFields_: null,
     77 
     78     // An array to hold cached password values.
     79     passwordValues_: null,
     80 
     81     /**
     82      * Initialize the scraper with given channel and docRoot. Note that the
     83      * scanning for password fields happens inside the function and does not
     84      * handle DOM tree changes after the call returns.
     85      * @param {!Object} channel The channel to send back password.
     86      * @param {!string} pageURL URL of the page.
     87      * @param {!HTMLElement} docRoot The root element of the DOM tree that
     88      *     contains the password fields of interest.
     89      */
     90     init: function(channel, pageURL, docRoot) {
     91       this.pageURL_ = pageURL;
     92       this.channel_ = channel;
     93 
     94       this.passwordFields_ = docRoot.querySelectorAll('input[type=password]');
     95       this.passwordValues_ = [];
     96 
     97       for (var i = 0; i < this.passwordFields_.length; ++i) {
     98         this.passwordFields_[i].addEventListener(
     99             'input', this.onPasswordChanged_.bind(this, i));
    100 
    101         this.passwordValues_[i] = this.passwordFields_[i].value;
    102       }
    103     },
    104 
    105     /**
    106      * Check if the password field at |index| has changed. If so, sends back
    107      * the updated value.
    108      */
    109     maybeSendUpdatedPassword: function(index) {
    110       var newValue = this.passwordFields_[index].value;
    111       if (newValue == this.passwordValues_[index])
    112         return;
    113 
    114       this.passwordValues_[index] = newValue;
    115 
    116       // Use an invalid char for URL as delimiter to concatenate page url and
    117       // password field index to construct a unique ID for the password field.
    118       var passwordId = this.pageURL_ + '|' + index;
    119       this.channel_.send({
    120         name: 'updatePassword',
    121         id: passwordId,
    122         password: newValue
    123       });
    124     },
    125 
    126     /**
    127      * Handles 'change' event in the scraped password fields.
    128      * @param {number} index The index of the password fields in
    129      *     |passwordFields_|.
    130      */
    131     onPasswordChanged_: function(index) {
    132       this.maybeSendUpdatedPassword(index);
    133     }
    134   };
    135 
    136   /**
    137    * Heuristic test whether the current page is a relevant SAML page.
    138    * Current implementation checks if it is a http or https page and has
    139    * some content in it.
    140    * @return {boolean} Whether the current page looks like a SAML page.
    141    */
    142   function isSAMLPage() {
    143     var url = window.location.href;
    144     if (!url.match(/^(http|https):\/\//))
    145       return false;
    146 
    147     return document.body.scrollWidth > 50 && document.body.scrollHeight > 50;
    148   }
    149 
    150   if (isSAMLPage()) {
    151     var pageURL = window.location.href;
    152 
    153     var channel = new Channel();
    154     channel.connect('injected');
    155     channel.send({name: 'pageLoaded', url: pageURL});
    156 
    157     var apiCallForwarder = new APICallForwarder();
    158     apiCallForwarder.init(channel);
    159 
    160     var passwordScraper = new PasswordInputScraper();
    161     passwordScraper.init(channel, pageURL, document.documentElement);
    162   }
    163 })();
    164