OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview | 6 * @fileoverview |
7 * OAuth2 class that handles retrieval/storage of an OAuth2 token. | 7 * OAuth2 class that handles retrieval/storage of an OAuth2 token. |
8 * | 8 * |
9 * Uses a content script to trampoline the OAuth redirect page back into the | 9 * Uses a content script to trampoline the OAuth redirect page back into the |
10 * extension context. This works around the lack of native support for | 10 * extension context. This works around the lack of native support for |
11 * chrome-extensions in OAuth2. | 11 * chrome-extensions in OAuth2. |
12 */ | 12 */ |
13 | 13 |
14 'use strict'; | 14 'use strict'; |
15 | 15 |
16 /** @suppress {duplicate} */ | 16 /** @suppress {duplicate} */ |
17 var remoting = remoting || {}; | 17 var remoting = remoting || {}; |
18 | 18 |
19 (function() { | 19 /** @type {remoting.OAuth2} */ |
| 20 remoting.oauth2 = null; |
| 21 |
| 22 |
20 /** @constructor */ | 23 /** @constructor */ |
21 remoting.OAuth2 = function() { | 24 remoting.OAuth2 = function() { |
22 } | 25 }; |
23 | 26 |
24 // Constants representing keys used for storing persistent state. | 27 // Constants representing keys used for storing persistent state. |
| 28 /** @private */ |
25 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; | 29 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; |
| 30 /** @private */ |
26 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; | 31 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; |
27 | 32 |
28 // Constants for parameters used in retrieving the OAuth2 credentials. | 33 // Constants for parameters used in retrieving the OAuth2 credentials. |
29 remoting.OAuth2.prototype.CLIENT_ID_ = | 34 /** @private */ remoting.OAuth2.prototype.CLIENT_ID_ = |
30 '440925447803-2pi3v45bff6tp1rde2f7q6lgbor3o5uj.' + | 35 '440925447803-2pi3v45bff6tp1rde2f7q6lgbor3o5uj.' + |
31 'apps.googleusercontent.com'; | 36 'apps.googleusercontent.com'; |
| 37 /** @private */ |
32 remoting.OAuth2.prototype.CLIENT_SECRET_ = 'W2ieEsG-R1gIA4MMurGrgMc_'; | 38 remoting.OAuth2.prototype.CLIENT_SECRET_ = 'W2ieEsG-R1gIA4MMurGrgMc_'; |
33 remoting.OAuth2.prototype.SCOPE_ = | 39 /** @private */ remoting.OAuth2.prototype.SCOPE_ = |
34 'https://www.googleapis.com/auth/chromoting ' + | 40 'https://www.googleapis.com/auth/chromoting ' + |
35 'https://www.googleapis.com/auth/googletalk ' + | 41 'https://www.googleapis.com/auth/googletalk ' + |
36 'https://www.googleapis.com/auth/userinfo#email'; | 42 'https://www.googleapis.com/auth/userinfo#email'; |
37 remoting.OAuth2.prototype.REDIRECT_URI_ = | 43 /** @private */ remoting.OAuth2.prototype.REDIRECT_URI_ = |
38 'https://talkgadget.google.com/talkgadget/blank'; | 44 'https://talkgadget.google.com/talkgadget/blank'; |
39 remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = | 45 /** @private */ remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = |
40 'https://accounts.google.com/o/oauth2/token'; | 46 'https://accounts.google.com/o/oauth2/token'; |
41 | 47 |
42 /** @return {boolean} True if the app is already authenticated. */ | 48 /** @return {boolean} True if the app is already authenticated. */ |
43 remoting.OAuth2.prototype.isAuthenticated = function() { | 49 remoting.OAuth2.prototype.isAuthenticated = function() { |
44 if (this.getRefreshToken()) { | 50 if (this.getRefreshToken()) { |
45 return true; | 51 return true; |
46 } | 52 } |
47 return false; | 53 return false; |
48 } | 54 }; |
49 | 55 |
50 /** | 56 /** |
51 * Removes all storage, and effectively unauthenticates the user. | 57 * Removes all storage, and effectively unauthenticates the user. |
52 * | 58 * |
53 * @return {void} Nothing. | 59 * @return {void} Nothing. |
54 */ | 60 */ |
55 remoting.OAuth2.prototype.clear = function() { | 61 remoting.OAuth2.prototype.clear = function() { |
56 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); | 62 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); |
57 this.clearAccessToken(); | 63 this.clearAccessToken(); |
58 } | 64 }; |
59 | 65 |
60 /** | 66 /** |
61 * @param {string} token The new refresh token. | 67 * @param {string} token The new refresh token. |
62 * @return {void} Nothing. | 68 * @return {void} Nothing. |
63 */ | 69 */ |
64 remoting.OAuth2.prototype.setRefreshToken = function(token) { | 70 remoting.OAuth2.prototype.setRefreshToken = function(token) { |
65 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); | 71 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); |
66 this.clearAccessToken(); | 72 this.clearAccessToken(); |
67 } | 73 }; |
68 | 74 |
69 /** @return {?string} The refresh token, if authenticated, or NULL. */ | 75 /** @return {?string} The refresh token, if authenticated, or NULL. */ |
70 remoting.OAuth2.prototype.getRefreshToken = function() { | 76 remoting.OAuth2.prototype.getRefreshToken = function() { |
71 var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); | 77 var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); |
72 if (typeof value == 'string') { | 78 if (typeof value == 'string') { |
73 return unescape(value); | 79 return unescape(value); |
74 } | 80 } |
75 return null; | 81 return null; |
76 } | 82 }; |
77 | 83 |
78 /** | 84 /** |
79 * @param {string} token The new access token. | 85 * @param {string} token The new access token. |
80 * @param {number} expiration Expiration time in milliseconds since epoch. | 86 * @param {number} expiration Expiration time in milliseconds since epoch. |
81 * @return {void} Nothing. | 87 * @return {void} Nothing. |
82 */ | 88 */ |
83 remoting.OAuth2.prototype.setAccessToken = function(token, expiration) { | 89 remoting.OAuth2.prototype.setAccessToken = function(token, expiration) { |
84 var access_token = {'token': token, 'expiration': expiration}; | 90 var access_token = {'token': token, 'expiration': expiration}; |
85 window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, | 91 window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, |
86 JSON.stringify(access_token)); | 92 JSON.stringify(access_token)); |
87 } | 93 }; |
88 | 94 |
89 /** | 95 /** |
90 * Returns the current access token, setting it to a invalid value if none | 96 * Returns the current access token, setting it to a invalid value if none |
91 * existed before. | 97 * existed before. |
92 * | 98 * |
| 99 * @private |
93 * @return {{token: string, expiration: number}} The current access token, or | 100 * @return {{token: string, expiration: number}} The current access token, or |
94 * an invalid token if not authenticated. | 101 * an invalid token if not authenticated. |
95 */ | 102 */ |
96 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { | 103 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { |
97 if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { | 104 if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { |
98 // Always be able to return structured data. | 105 // Always be able to return structured data. |
99 this.setAccessToken('', 0); | 106 this.setAccessToken('', 0); |
100 } | 107 } |
101 var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_); | 108 var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_); |
102 if (typeof accessToken == 'string') { | 109 if (typeof accessToken == 'string') { |
103 var result = JSON.parse(accessToken); | 110 var result = JSON.parse(accessToken); |
104 if ('token' in result && 'expiration' in result) { | 111 if ('token' in result && 'expiration' in result) { |
105 return /** @type {{token: string, expiration: number}} */ result; | 112 return /** @type {{token: string, expiration: number}} */ result; |
106 } | 113 } |
107 } | 114 } |
108 console.log('Invalid access token stored.'); | 115 console.log('Invalid access token stored.'); |
109 return {'token': '', 'expiration': 0}; | 116 return {'token': '', 'expiration': 0}; |
110 } | 117 }; |
111 | 118 |
112 /** | 119 /** |
113 * Returns true if the access token is expired, or otherwise invalid. | 120 * Returns true if the access token is expired, or otherwise invalid. |
114 * | 121 * |
115 * Will throw if !isAuthenticated(). | 122 * Will throw if !isAuthenticated(). |
116 * | 123 * |
117 * @return {boolean} True if a new access token is needed. | 124 * @return {boolean} True if a new access token is needed. |
118 */ | 125 */ |
119 remoting.OAuth2.prototype.needsNewAccessToken = function() { | 126 remoting.OAuth2.prototype.needsNewAccessToken = function() { |
120 if (!this.isAuthenticated()) { | 127 if (!this.isAuthenticated()) { |
121 throw 'Not Authenticated.'; | 128 throw 'Not Authenticated.'; |
122 } | 129 } |
123 var access_token = this.getAccessTokenInternal_(); | 130 var access_token = this.getAccessTokenInternal_(); |
124 if (!access_token['token']) { | 131 if (!access_token['token']) { |
125 return true; | 132 return true; |
126 } | 133 } |
127 if (Date.now() > access_token['expiration']) { | 134 if (Date.now() > access_token['expiration']) { |
128 return true; | 135 return true; |
129 } | 136 } |
130 return false; | 137 return false; |
131 } | 138 }; |
132 | 139 |
133 /** | 140 /** |
134 * Returns the current access token. | 141 * Returns the current access token. |
135 * | 142 * |
136 * Will throw if !isAuthenticated() or needsNewAccessToken(). | 143 * Will throw if !isAuthenticated() or needsNewAccessToken(). |
137 * | 144 * |
138 * @return {{token: string, expiration: number}} | 145 * @return {string} The access token. |
139 */ | 146 */ |
140 remoting.OAuth2.prototype.getAccessToken = function() { | 147 remoting.OAuth2.prototype.getAccessToken = function() { |
141 if (this.needsNewAccessToken()) { | 148 if (this.needsNewAccessToken()) { |
142 throw 'Access Token expired.'; | 149 throw 'Access Token expired.'; |
143 } | 150 } |
144 return this.getAccessTokenInternal_()['token']; | 151 return this.getAccessTokenInternal_()['token']; |
145 } | 152 }; |
146 | 153 |
147 /** @return {void} Nothing. */ | 154 /** @return {void} Nothing. */ |
148 remoting.OAuth2.prototype.clearAccessToken = function() { | 155 remoting.OAuth2.prototype.clearAccessToken = function() { |
149 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); | 156 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); |
150 } | 157 }; |
151 | 158 |
152 /** | 159 /** |
153 * Update state based on token response from the OAuth2 /token endpoint. | 160 * Update state based on token response from the OAuth2 /token endpoint. |
154 * | 161 * |
| 162 * @private |
155 * @param {XMLHttpRequest} xhr The XHR object for this request. | 163 * @param {XMLHttpRequest} xhr The XHR object for this request. |
156 * @return {void} Nothing. | 164 * @return {void} Nothing. |
157 */ | 165 */ |
158 remoting.OAuth2.prototype.processTokenResponse_ = function(xhr) { | 166 remoting.OAuth2.prototype.processTokenResponse_ = function(xhr) { |
159 if (xhr.status == 200) { | 167 if (xhr.status == 200) { |
160 var tokens = JSON.parse(xhr.responseText); | 168 var tokens = JSON.parse(xhr.responseText); |
161 if ('refresh_token' in tokens) { | 169 if ('refresh_token' in tokens) { |
162 this.setRefreshToken(tokens['refresh_token']); | 170 this.setRefreshToken(tokens['refresh_token']); |
163 } | 171 } |
164 | 172 |
165 // Offset by 120 seconds so that we can guarantee that the token | 173 // Offset by 120 seconds so that we can guarantee that the token |
166 // we return will be valid for at least 2 minutes. | 174 // we return will be valid for at least 2 minutes. |
167 // If the access token is to be useful, this object must make some | 175 // If the access token is to be useful, this object must make some |
168 // guarantee as to how long the token will be valid for. | 176 // guarantee as to how long the token will be valid for. |
169 // The choice of 2 minutes is arbitrary, but that length of time | 177 // The choice of 2 minutes is arbitrary, but that length of time |
170 // is part of the contract satisfied by callWithToken(). | 178 // is part of the contract satisfied by callWithToken(). |
171 // Offset by a further 30 seconds to account for RTT issues. | 179 // Offset by a further 30 seconds to account for RTT issues. |
172 this.setAccessToken(tokens['access_token'], | 180 this.setAccessToken(tokens['access_token'], |
173 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); | 181 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); |
174 } else { | 182 } else { |
175 console.log('Failed to get tokens. Status: ' + xhr.status + | 183 console.log('Failed to get tokens. Status: ' + xhr.status + |
176 ' response: ' + xhr.responseText); | 184 ' response: ' + xhr.responseText); |
177 } | 185 } |
178 } | 186 }; |
179 | 187 |
180 /** | 188 /** |
181 * Asynchronously retrieves a new access token from the server. | 189 * Asynchronously retrieves a new access token from the server. |
182 * | 190 * |
183 * Will throw if !isAuthenticated(). | 191 * Will throw if !isAuthenticated(). |
184 * | 192 * |
185 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on | 193 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on |
186 * completion. | 194 * completion. |
187 * @return {void} Nothing. | 195 * @return {void} Nothing. |
188 */ | 196 */ |
189 remoting.OAuth2.prototype.refreshAccessToken = function(onDone) { | 197 remoting.OAuth2.prototype.refreshAccessToken = function(onDone) { |
190 if (!this.isAuthenticated()) { | 198 if (!this.isAuthenticated()) { |
191 throw 'Not Authenticated.'; | 199 throw 'Not Authenticated.'; |
192 } | 200 } |
193 | 201 |
194 var parameters = { | 202 var parameters = { |
195 'client_id': this.CLIENT_ID_, | 203 'client_id': this.CLIENT_ID_, |
196 'client_secret': this.CLIENT_SECRET_, | 204 'client_secret': this.CLIENT_SECRET_, |
197 'refresh_token': this.getRefreshToken(), | 205 'refresh_token': this.getRefreshToken(), |
198 'grant_type': 'refresh_token' | 206 'grant_type': 'refresh_token' |
199 }; | 207 }; |
200 | 208 |
| 209 /** @type {remoting.OAuth2} */ |
201 var that = this; | 210 var that = this; |
| 211 /** @param {XMLHttpRequest} xhr The XHR reply. */ |
| 212 var processTokenResponse = function(xhr) { |
| 213 that.processTokenResponse_(xhr); |
| 214 onDone(xhr); |
| 215 }; |
202 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, | 216 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, |
203 function(xhr) { | 217 processTokenResponse, |
204 that.processTokenResponse_(xhr); | |
205 onDone(xhr); | |
206 }, | |
207 parameters); | 218 parameters); |
208 } | 219 }; |
209 | 220 |
210 /** | 221 /** |
211 * Redirect page to get a new OAuth2 Refresh Token. | 222 * Redirect page to get a new OAuth2 Refresh Token. |
212 * | 223 * |
213 * @return {void} Nothing. | 224 * @return {void} Nothing. |
214 */ | 225 */ |
215 remoting.OAuth2.prototype.doAuthRedirect = function() { | 226 remoting.OAuth2.prototype.doAuthRedirect = function() { |
216 var GET_CODE_URL = 'https://accounts.google.com/o/oauth2/auth?' + | 227 var GET_CODE_URL = 'https://accounts.google.com/o/oauth2/auth?' + |
217 remoting.xhr.urlencodeParamHash({ | 228 remoting.xhr.urlencodeParamHash({ |
218 'client_id': this.CLIENT_ID_, | 229 'client_id': this.CLIENT_ID_, |
219 'redirect_uri': this.REDIRECT_URI_, | 230 'redirect_uri': this.REDIRECT_URI_, |
220 'scope': this.SCOPE_, | 231 'scope': this.SCOPE_, |
221 'response_type': 'code' | 232 'response_type': 'code' |
222 }); | 233 }); |
223 window.location.replace(GET_CODE_URL); | 234 window.location.replace(GET_CODE_URL); |
224 } | 235 }; |
225 | 236 |
226 /** | 237 /** |
227 * Asynchronously exchanges an authorization code for a refresh token. | 238 * Asynchronously exchanges an authorization code for a refresh token. |
228 * | 239 * |
229 * @param {string} code The new refresh token. | 240 * @param {string} code The new refresh token. |
230 * @param {function(XMLHttpRequest):void} onDone Callback to invoke on | 241 * @param {function(XMLHttpRequest):void} onDone Callback to invoke on |
231 * completion. | 242 * completion. |
232 * @return {void} Nothing. | 243 * @return {void} Nothing. |
233 */ | 244 */ |
234 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) { | 245 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) { |
235 var parameters = { | 246 var parameters = { |
236 'client_id': this.CLIENT_ID_, | 247 'client_id': this.CLIENT_ID_, |
237 'client_secret': this.CLIENT_SECRET_, | 248 'client_secret': this.CLIENT_SECRET_, |
238 'redirect_uri': this.REDIRECT_URI_, | 249 'redirect_uri': this.REDIRECT_URI_, |
239 'code': code, | 250 'code': code, |
240 'grant_type': 'authorization_code' | 251 'grant_type': 'authorization_code' |
241 }; | 252 }; |
242 | 253 |
| 254 /** @type {remoting.OAuth2} */ |
243 var that = this; | 255 var that = this; |
| 256 /** @param {XMLHttpRequest} xhr The XHR reply. */ |
| 257 var processTokenResponse = function(xhr) { |
| 258 that.processTokenResponse_(xhr); |
| 259 onDone(xhr); |
| 260 }; |
244 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, | 261 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, |
245 function(xhr) { | 262 processTokenResponse, |
246 that.processTokenResponse_(xhr); | |
247 onDone(xhr); | |
248 }, | |
249 parameters); | 263 parameters); |
250 } | 264 }; |
251 | 265 |
252 /** | 266 /** |
253 * Call myfunc with an access token as the only parameter. | 267 * Call myfunc with an access token as the only parameter. |
254 * | 268 * |
255 * This will refresh the access token if necessary. If the access token | 269 * This will refresh the access token if necessary. If the access token |
256 * cannot be refreshed, an error is thrown. | 270 * cannot be refreshed, an error is thrown. |
257 * | 271 * |
258 * The access token will remain valid for at least 2 minutes. | 272 * The access token will remain valid for at least 2 minutes. |
259 * | 273 * |
260 * @param {function({token: string, expiration: number}):void} myfunc | 274 * @param {function(string):void} myfunc |
261 * Function to invoke with access token. | 275 * Function to invoke with access token. |
262 * @return {void} Nothing. | 276 * @return {void} Nothing. |
263 */ | 277 */ |
264 remoting.OAuth2.prototype.callWithToken = function(myfunc) { | 278 remoting.OAuth2.prototype.callWithToken = function(myfunc) { |
| 279 /** @type {remoting.OAuth2} */ |
265 var that = this; | 280 var that = this; |
266 if (remoting.oauth2.needsNewAccessToken()) { | 281 if (remoting.oauth2.needsNewAccessToken()) { |
267 remoting.oauth2.refreshAccessToken(function() { | 282 remoting.oauth2.refreshAccessToken(function() { |
268 if (remoting.oauth2.needsNewAccessToken()) { | 283 if (remoting.oauth2.needsNewAccessToken()) { |
269 // If we still need it, we're going to infinite loop. | 284 // If we still need it, we're going to infinite loop. |
270 throw 'Unable to get access token.'; | 285 throw 'Unable to get access token.'; |
271 } | 286 } |
272 myfunc(that.getAccessToken()); | 287 myfunc(that.getAccessToken()); |
273 }); | 288 }); |
274 return; | 289 return; |
275 } | 290 } |
276 | 291 |
277 myfunc(this.getAccessToken()); | 292 myfunc(this.getAccessToken()); |
278 } | 293 }; |
279 }()); | |
OLD | NEW |