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} */
24var 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 */
37remoting.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 */
62remoting.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 */
92remoting.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 */
135remoting.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 */
151remoting.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 */
169remoting.ThirdPartyTokenFetcher.prototype.fetchTokenIdentityApi_ = function() {
170  var fullTokenUrl = this.getFullTokenUrl_();
171  chrome.identity.launchWebAuthFlow(
172    {'url': fullTokenUrl, 'interactive': true},
173    this.parseRedirectUrl_.bind(this));
174};