Home | History | Annotate | Download | only in webapp
      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} */
     13 var remoting = remoting || {};
     14 
     15 /** @constructor */
     16 remoting.OAuth2Api = function() {
     17 };
     18 
     19 /** @private
     20  *  @return {string} OAuth2 token URL.
     21  */
     22 remoting.OAuth2Api.getOAuth2TokenEndpoint_ = function() {
     23   return remoting.settings.OAUTH2_BASE_URL + '/token';
     24 };
     25 
     26 /** @private
     27  *  @return {string} OAuth token revocation URL.
     28  */
     29 remoting.OAuth2Api.getOAuth2RevokeTokenEndpoint_ = function() {
     30   return remoting.settings.OAUTH2_BASE_URL + '/revoke';
     31 };
     32 
     33 /** @private
     34  *  @return {string} OAuth2 userinfo API URL.
     35  */
     36 remoting.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  */
     48 remoting.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  */
     78 remoting.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  */
    126 remoting.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  */
    171 remoting.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  */
    204 remoting.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