1c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez// Copyright 2013 The Chromium Authors. All rights reserved. 2c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez// Use of this source code is governed by a BSD-style license that can be 3c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez// found in the LICENSE file. 4c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 5c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** 6c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @fileoverview 7c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * OAuth2 API flow implementations. 8c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez */ 9c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 10c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez'use strict'; 11c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 12c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** @suppress {duplicate} */ 13c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezvar remoting = remoting || {}; 14c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 15c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** @constructor */ 16c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezremoting.OAuth2Api = function() { 17c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez}; 18c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 19c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** @private 20c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @return {string} OAuth2 token URL. 21c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez */ 22c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezremoting.OAuth2Api.getOAuth2TokenEndpoint_ = function() { 23c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez return remoting.settings.OAUTH2_BASE_URL + '/token'; 24c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez}; 25c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 26c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** @private 27c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @return {string} OAuth2 userinfo API URL. 28c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez */ 29c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezremoting.OAuth2Api.getOAuth2ApiUserInfoEndpoint_ = function() { 30c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez return remoting.settings.OAUTH2_API_BASE_URL + '/v1/userinfo'; 31c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez}; 32c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 33c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 34c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** 35c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * Interprets HTTP error responses in authentication XMLHttpRequests. 36c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * 37c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @private 38c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {number} xhrStatus Status (HTTP response code) of the XMLHttpRequest. 39c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @return {remoting.Error} An error code to be raised. 40c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez */ 41c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezremoting.OAuth2Api.interpretXhrStatus_ = 42c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez function(xhrStatus) { 43c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // Return AUTHENTICATION_FAILED by default, so that the user can try to 44c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // recover from an unexpected failure by signing in again. 45c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez /** @type {remoting.Error} */ 46c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var error = remoting.Error.AUTHENTICATION_FAILED; 47c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez if (xhrStatus == 400 || xhrStatus == 401 || xhrStatus == 403) { 48c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez error = remoting.Error.AUTHENTICATION_FAILED; 49c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } else if (xhrStatus == 502 || xhrStatus == 503) { 50c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez error = remoting.Error.SERVICE_UNAVAILABLE; 51c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } else if (xhrStatus == 0) { 52c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez error = remoting.Error.NETWORK_FAILURE; 53c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } else { 54c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez console.warn('Unexpected authentication response code: ' + xhrStatus); 55c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } 56c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez return error; 57c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez}; 58c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 59c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** 60c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * Asynchronously retrieves a new access token from the server. 61c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * 62c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {function(string, number): void} onDone Callback to invoke when 63c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * the access token and expiration time are successfully fetched. 64c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {function(remoting.Error):void} onError Callback invoked if an 65c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * error occurs. 66c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} clientId OAuth2 client ID. 67c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} clientSecret OAuth2 client secret. 68c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} refreshToken OAuth2 refresh token to be redeemed. 69c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @return {void} Nothing. 70c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez */ 71c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezremoting.OAuth2Api.refreshAccessToken = function( 72c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onDone, onError, clientId, clientSecret, refreshToken) { 73c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez /** @param {XMLHttpRequest} xhr */ 74c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var onResponse = function(xhr) { 75c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez if (xhr.status == 200) { 76c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez try { 77c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // Don't use jsonParseSafe here unless you move the definition out of 78c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // remoting.js, otherwise this won't work from the OAuth trampoline. 79c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // TODO(jamiewalch): Fix this once we're no longer using the trampoline. 80c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var tokens = JSON.parse(xhr.responseText); 81c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onDone(tokens['access_token'], tokens['expires_in']); 82c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } catch (err) { 83c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez console.error('Invalid "token" response from server:', 84c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez /** @type {*} */ (err)); 85c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onError(remoting.Error.UNEXPECTED); 86c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } 87c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } else { 88c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez console.error('Failed to refresh token. Status: ' + xhr.status + 89c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez ' response: ' + xhr.responseText); 90c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onError(remoting.OAuth2Api.interpretXhrStatus_(xhr.status)); 91c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } 92c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez }; 93c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 94c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var parameters = { 95c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'client_id': clientId, 96c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'client_secret': clientSecret, 97c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'refresh_token': refreshToken, 98c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'grant_type': 'refresh_token' 99c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez }; 100c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 101c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez remoting.xhr.post(remoting.OAuth2Api.getOAuth2TokenEndpoint_(), 102c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onResponse, parameters); 103c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez}; 104c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 105c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** 106c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * Asynchronously exchanges an authorization code for access and refresh tokens. 107c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * 108c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {function(string, string, number): void} onDone Callback to 109c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * invoke when the refresh token, access token and access token expiration 110c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * time are successfully fetched. 111c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {function(remoting.Error):void} onError Callback invoked if an 112c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * error occurs. 113c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} clientId OAuth2 client ID. 114c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} clientSecret OAuth2 client secret. 115c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} code OAuth2 authorization code. 116c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} redirectUri Redirect URI used to obtain this code. 117c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @return {void} Nothing. 118c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez */ 119c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezremoting.OAuth2Api.exchangeCodeForTokens = function( 120c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onDone, onError, clientId, clientSecret, code, redirectUri) { 121c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez /** @param {XMLHttpRequest} xhr */ 122c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var onResponse = function(xhr) { 123c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez if (xhr.status == 200) { 124c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez try { 125c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // Don't use jsonParseSafe here unless you move the definition out of 126c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // remoting.js, otherwise this won't work from the OAuth trampoline. 127c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez // TODO(jamiewalch): Fix this once we're no longer using the trampoline. 128c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var tokens = JSON.parse(xhr.responseText); 129c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onDone(tokens['refresh_token'], 130c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez tokens['access_token'], tokens['expires_in']); 131c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } catch (err) { 132c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez console.error('Invalid "token" response from server:', 133c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez /** @type {*} */ (err)); 134c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onError(remoting.Error.UNEXPECTED); 135c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } 136c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } else { 137c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez console.error('Failed to exchange code for token. Status: ' + xhr.status + 138c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez ' response: ' + xhr.responseText); 139c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onError(remoting.OAuth2Api.interpretXhrStatus_(xhr.status)); 140c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } 141c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez }; 142c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 143c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var parameters = { 144c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'client_id': clientId, 145c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'client_secret': clientSecret, 146c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'redirect_uri': redirectUri, 147c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'code': code, 148c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 'grant_type': 'authorization_code' 149c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez }; 150c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez remoting.xhr.post(remoting.OAuth2Api.getOAuth2TokenEndpoint_(), 151c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onResponse, parameters); 152c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez}; 153c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez 154c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez/** 155c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * Get the user's email address. 156c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * 157c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {function(string):void} onDone Callback invoked when the email 158c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * address is available. 159c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {function(remoting.Error):void} onError Callback invoked if an 160c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * error occurs. 161c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @param {string} token Access token. 162c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez * @return {void} Nothing. 163c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez */ 164c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerezremoting.OAuth2Api.getEmail = function(onDone, onError, token) { 165c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez /** @param {XMLHttpRequest} xhr */ 166c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var onResponse = function(xhr) { 167c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez if (xhr.status == 200) { 168c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez try { 169c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez var result = JSON.parse(xhr.responseText); 170c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onDone(result['email']); 171c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } catch (err) { 172c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez console.error('Invalid "userinfo" response from server:', 173c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez /** @type {*} */ (err)); 174c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez onError(remoting.Error.UNEXPECTED); 175c6db1b3396384186aab5b685fe1fd540e17b3a62Francisco Jerez } 176 } else { 177 console.error('Failed to get email. Status: ' + xhr.status + 178 ' response: ' + xhr.responseText); 179 onError(remoting.OAuth2Api.interpretXhrStatus_(xhr.status)); 180 } 181 }; 182 var headers = { 'Authorization': 'OAuth ' + token }; 183 remoting.xhr.get(remoting.OAuth2Api.getOAuth2ApiUserInfoEndpoint_(), 184 onResponse, '', headers); 185}; 186