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