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