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