Home | History | Annotate | Download | only in webapp
      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  * Third party authentication support for the remoting web-app.
      8  *
      9  * When third party authentication is being used, the client must request both a
     10  * token and a shared secret from a third-party server. The server can then
     11  * present the user with an authentication page, or use any other method to
     12  * authenticate the user via the browser. Once the user is authenticated, the
     13  * server will redirect the browser to a URL containing the token and shared
     14  * secret in its fragment. The client then sends only the token to the host.
     15  * The host signs the token, then contacts the third-party server to exchange
     16  * the token for the shared secret. Once both client and host have the shared
     17  * secret, they use a zero-disclosure mutual authentication protocol to
     18  * negotiate an authentication key, which is used to establish the connection.
     19  */
     20 
     21 'use strict';
     22 
     23 /** @suppress {duplicate} */
     24 var remoting = remoting || {};
     25 
     26 /**
     27  * @constructor
     28  * Encapsulates the logic to fetch a third party authentication token.
     29  *
     30  * @param {string} tokenUrl Token-issue URL received from the host.
     31  * @param {string} hostPublicKey Host public key (DER and Base64 encoded).
     32  * @param {string} scope OAuth scope to request the token for.
     33  * @param {Array.<string>} tokenUrlPatterns Token URL patterns allowed for the
     34  *     domain, received from the directory server.
     35  * @param {function(string, string):void} onThirdPartyTokenFetched Callback.
     36  */
     37 remoting.ThirdPartyTokenFetcher = function(
     38     tokenUrl, hostPublicKey, scope, tokenUrlPatterns,
     39     onThirdPartyTokenFetched) {
     40   this.tokenUrl_ = tokenUrl;
     41   this.tokenScope_ = scope;
     42   this.onThirdPartyTokenFetched_ = onThirdPartyTokenFetched;
     43   this.failFetchToken_ = function() { onThirdPartyTokenFetched('', ''); };
     44   this.xsrfToken_ = remoting.generateXsrfToken();
     45   this.tokenUrlPatterns_ = tokenUrlPatterns;
     46   this.hostPublicKey_ = hostPublicKey;
     47   if (chrome.identity) {
     48     /** @type {function():void}
     49      * @private */
     50     this.fetchTokenInternal_ = this.fetchTokenIdentityApi_.bind(this);
     51     this.redirectUri_ = 'https://' + window.location.hostname +
     52         '.chromiumapp.org/ThirdPartyAuth';
     53   } else {
     54     this.fetchTokenInternal_ = this.fetchTokenWindowOpen_.bind(this);
     55     this.redirectUri_ = remoting.settings.THIRD_PARTY_AUTH_REDIRECT_URI;
     56   }
     57 };
     58 
     59 /**
     60  * Fetch a token with the parameters configured in this object.
     61  */
     62 remoting.ThirdPartyTokenFetcher.prototype.fetchToken = function() {
     63   // If there is no list of patterns, this host cannot use a token URL.
     64   if (!this.tokenUrlPatterns_) {
     65     console.error('No token URLs are allowed for this host');
     66     this.failFetchToken_();
     67   }
     68 
     69   // Verify the host-supplied URL matches the domain's allowed URL patterns.
     70   for (var i = 0; i < this.tokenUrlPatterns_.length; i++) {
     71     if (this.tokenUrl_.match(this.tokenUrlPatterns_[i])) {
     72       var hostPermissions = new remoting.ThirdPartyHostPermissions(
     73           this.tokenUrl_);
     74       hostPermissions.getPermission(
     75           this.fetchTokenInternal_,
     76           this.failFetchToken_);
     77       return;
     78     }
     79   }
     80   // If the URL doesn't match any pattern in the list, refuse to access it.
     81   console.error('Token URL does not match the domain\'s allowed URL patterns.' +
     82       ' URL: ' + this.tokenUrl_ + ', patterns: ' + this.tokenUrlPatterns_);
     83   this.failFetchToken_();
     84 };
     85 
     86 /**
     87  * Parse the access token from the URL to which we were redirected.
     88  *
     89  * @param {string} responseUrl The URL to which we were redirected.
     90  * @private
     91  */
     92 remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ =
     93     function(responseUrl) {
     94   var token = '';
     95   var sharedSecret = '';
     96 
     97   if (responseUrl && responseUrl.search('#') >= 0) {
     98     var query = responseUrl.substring(responseUrl.search('#') + 1);
     99     var parts = query.split('&');
    100     /** @type {Object.<string>} */
    101     var queryArgs = {};
    102     for (var i = 0; i < parts.length; i++) {
    103       var pair = parts[i].split('=');
    104       queryArgs[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
    105     }
    106 
    107     // Check that 'state' contains the same XSRF token we sent in the request.
    108     if ('state' in queryArgs && queryArgs['state'] == this.xsrfToken_ &&
    109         'code' in queryArgs && 'access_token' in queryArgs) {
    110       // Terminology note:
    111       // In the OAuth code/token exchange semantics, 'code' refers to the value
    112       // obtained when the *user* authenticates itself, while 'access_token' is
    113       // the value obtained when the *application* authenticates itself to the
    114       // server ("implicitly", by receiving it directly in the URL fragment, or
    115       // explicitly, by sending the 'code' and a 'client_secret' to the server).
    116       // Internally, the piece of data obtained when the user authenticates
    117       // itself is called the 'token', and the one obtained when the host
    118       // authenticates itself (using the 'token' received from the client and
    119       // its private key) is called the 'shared secret'.
    120       // The client implicitly authenticates itself, and directly obtains the
    121       // 'shared secret', along with the 'token' from the redirect URL fragment.
    122       token = queryArgs['code'];
    123       sharedSecret = queryArgs['access_token'];
    124     }
    125   }
    126   this.onThirdPartyTokenFetched_(token, sharedSecret);
    127 };
    128 
    129 /**
    130  * Build a full token request URL from the parameters in this object.
    131  *
    132  * @return {string} Full URL to request a token.
    133  * @private
    134  */
    135 remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() {
    136   return this.tokenUrl_ + '?' + remoting.xhr.urlencodeParamHash({
    137     'redirect_uri': this.redirectUri_,
    138     'scope': this.tokenScope_,
    139     'client_id': this.hostPublicKey_,
    140     // The webapp uses an "implicit" OAuth flow with multiple response types to
    141     // obtain both the code and the shared secret in a single request.
    142     'response_type': 'code token',
    143     'state': this.xsrfToken_
    144   });
    145 };
    146 
    147 /**
    148  * Fetch a token by opening a new window and redirecting to a content script.
    149  * @private
    150  */
    151 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenWindowOpen_ = function() {
    152   /** @type {remoting.ThirdPartyTokenFetcher} */
    153   var that = this;
    154   var fullTokenUrl = this.getFullTokenUrl_();
    155   // The function below can't be anonymous, since it needs to reference itself.
    156   /** @param {string} message Message received from the content script. */
    157   function tokenMessageListener(message) {
    158     that.parseRedirectUrl_(message);
    159     chrome.extension.onMessage.removeListener(tokenMessageListener);
    160   }
    161   chrome.extension.onMessage.addListener(tokenMessageListener);
    162   window.open(fullTokenUrl, '_blank', 'location=yes,toolbar=no,menubar=no');
    163 };
    164 
    165 /**
    166  * Fetch a token from a token server using the identity.launchWebAuthFlow API.
    167  * @private
    168  */
    169 remoting.ThirdPartyTokenFetcher.prototype.fetchTokenIdentityApi_ = function() {
    170   var fullTokenUrl = this.getFullTokenUrl_();
    171   chrome.identity.launchWebAuthFlow(
    172     {'url': fullTokenUrl, 'interactive': true},
    173     this.parseRedirectUrl_.bind(this));
    174 };