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