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.
7 * Third party authentication support for the remoting web-app.
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.
23 /** @suppress {duplicate} */
24 var remoting
= remoting
|| {};
28 * Encapsulates the logic to fetch a third party authentication token.
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.
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_
= base
.generateXsrfToken();
45 this.tokenUrlPatterns_
= tokenUrlPatterns
;
46 this.hostPublicKey_
= hostPublicKey
;
47 if (chrome
.identity
) {
48 /** @type {function():void}
50 this.fetchTokenInternal_
= this.fetchTokenIdentityApi_
.bind(this);
51 this.redirectUri_
= 'https://' + window
.location
.hostname
+
52 '.chromiumapp.org/ThirdPartyAuth';
54 this.fetchTokenInternal_
= this.fetchTokenWindowOpen_
.bind(this);
55 this.redirectUri_
= remoting
.settings
.THIRD_PARTY_AUTH_REDIRECT_URI
;
60 * Fetch a token with the parameters configured in this object.
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_();
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(
74 hostPermissions
.getPermission(
75 this.fetchTokenInternal_
,
76 this.failFetchToken_
);
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_();
87 * Parse the access token from the URL to which we were redirected.
89 * @param {string} responseUrl The URL to which we were redirected.
92 remoting
.ThirdPartyTokenFetcher
.prototype.parseRedirectUrl_
=
93 function(responseUrl
) {
95 var sharedSecret
= '';
97 if (responseUrl
&& responseUrl
.search('#') >= 0) {
98 var query
= responseUrl
.substring(responseUrl
.search('#') + 1);
99 var parts
= query
.split('&');
100 /** @type {Object.<string>} */
102 for (var i
= 0; i
< parts
.length
; i
++) {
103 var pair
= parts
[i
].split('=');
104 queryArgs
[decodeURIComponent(pair
[0])] = decodeURIComponent(pair
[1]);
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
) {
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'];
126 this.onThirdPartyTokenFetched_(token
, sharedSecret
);
130 * Build a full token request URL from the parameters in this object.
132 * @return {string} Full URL to request a token.
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_
148 * Fetch a token by opening a new window and redirecting to a content script.
151 remoting
.ThirdPartyTokenFetcher
.prototype.fetchTokenWindowOpen_ = function() {
152 /** @type {remoting.ThirdPartyTokenFetcher} */
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
);
161 chrome
.extension
.onMessage
.addListener(tokenMessageListener
);
162 window
.open(fullTokenUrl
, '_blank', 'location=yes,toolbar=no,menubar=no');
166 * Fetch a token from a token server using the identity.launchWebAuthFlow API.
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));