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