| /** |
| * @license |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| (function(window) { |
| 'use strict'; |
| |
| // Prevent redefinition. |
| if (window.Gerrit.Auth) { return; } |
| |
| const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s |
| const MAX_GET_TOKEN_RETRIES = 2; |
| |
| /** |
| * Auth class. |
| * |
| * Gerrit.Auth is an instance of this class. |
| */ |
| class Auth { |
| constructor() { |
| this._type = null; |
| this._cachedTokenPromise = null; |
| this._defaultOptions = {}; |
| this._retriesLeft = MAX_GET_TOKEN_RETRIES; |
| this._status = Auth.STATUS.UNDETERMINED; |
| this._authCheckPromise = null; |
| this._last_auth_check_time = Date.now(); |
| } |
| |
| get baseUrl() { |
| return Gerrit.BaseUrlBehavior.getBaseUrl(); |
| } |
| |
| /** |
| * Returns if user is authed or not. |
| * |
| * @returns {!Promise<boolean>} |
| */ |
| authCheck() { |
| if (!this._authCheckPromise || |
| (Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS) |
| ) { |
| // Refetch after last check expired |
| this._authCheckPromise = fetch(`${this.baseUrl}/auth-check`); |
| this._last_auth_check_time = Date.now(); |
| } |
| |
| return this._authCheckPromise.then(res => { |
| // auth-check will return 204 if authed |
| // treat the rest as unauthed |
| if (res.status === 204) { |
| this._setStatus(Auth.STATUS.AUTHED); |
| return true; |
| } else { |
| this._setStatus(Auth.STATUS.NOT_AUTHED); |
| return false; |
| } |
| }).catch(e => { |
| this._setStatus(Auth.STATUS.ERROR); |
| // Reset _authCheckPromise to avoid caching the failed promise |
| this._authCheckPromise = null; |
| return false; |
| }); |
| } |
| |
| clearCache() { |
| this._authCheckPromise = null; |
| } |
| |
| /** |
| * @param {Auth.STATUS} status |
| */ |
| _setStatus(status) { |
| if (this._status === status) return; |
| |
| if (this._status === Auth.STATUS.AUTHED) { |
| Gerrit.emit('auth-error', { |
| message: Auth.CREDS_EXPIRED_MSG, action: 'Refresh credentials', |
| }); |
| } |
| this._status = status; |
| } |
| |
| get status() { |
| return this._status; |
| } |
| |
| get isAuthed() { |
| return this._status === Auth.STATUS.AUTHED; |
| } |
| |
| _getToken() { |
| return Promise.resolve(this._cachedTokenPromise); |
| } |
| |
| /** |
| * Enable cross-domain authentication using OAuth access token. |
| * |
| * @param { |
| * function(): !Promise<{ |
| * access_token: string, |
| * expires_at: number |
| * }> |
| * } getToken |
| * @param {?{credentials:string}} defaultOptions |
| */ |
| setup(getToken, defaultOptions) { |
| this._retriesLeft = MAX_GET_TOKEN_RETRIES; |
| if (getToken) { |
| this._type = Auth.TYPE.ACCESS_TOKEN; |
| this._cachedTokenPromise = null; |
| this._getToken = getToken; |
| } |
| this._defaultOptions = {}; |
| if (defaultOptions) { |
| for (const p of ['credentials']) { |
| this._defaultOptions[p] = defaultOptions[p]; |
| } |
| } |
| } |
| |
| /** |
| * Perform network fetch with authentication. |
| * |
| * @param {string} url |
| * @param {Object=} opt_options |
| * @return {!Promise<!Response>} |
| */ |
| fetch(url, opt_options) { |
| const options = Object.assign({ |
| headers: new Headers(), |
| }, this._defaultOptions, opt_options); |
| if (this._type === Auth.TYPE.ACCESS_TOKEN) { |
| return this._getAccessToken().then( |
| accessToken => |
| this._fetchWithAccessToken(url, options, accessToken) |
| ); |
| } else { |
| return this._fetchWithXsrfToken(url, options); |
| } |
| } |
| |
| _getCookie(name) { |
| const key = name + '='; |
| let result = ''; |
| document.cookie.split(';').some(c => { |
| c = c.trim(); |
| if (c.startsWith(key)) { |
| result = c.substring(key.length); |
| return true; |
| } |
| }); |
| return result; |
| } |
| |
| _isTokenValid(token) { |
| if (!token) { return false; } |
| if (!token.access_token || !token.expires_at) { return false; } |
| |
| const expiration = new Date(parseInt(token.expires_at, 10) * 1000); |
| if (Date.now() >= expiration.getTime()) { return false; } |
| |
| return true; |
| } |
| |
| _fetchWithXsrfToken(url, options) { |
| if (options.method && options.method !== 'GET') { |
| const token = this._getCookie('XSRF_TOKEN'); |
| if (token) { |
| options.headers.append('X-Gerrit-Auth', token); |
| } |
| } |
| options.credentials = 'same-origin'; |
| return fetch(url, options); |
| } |
| |
| /** |
| * @return {!Promise<string>} |
| */ |
| _getAccessToken() { |
| if (!this._cachedTokenPromise) { |
| this._cachedTokenPromise = this._getToken(); |
| } |
| return this._cachedTokenPromise.then(token => { |
| if (this._isTokenValid(token)) { |
| this._retriesLeft = MAX_GET_TOKEN_RETRIES; |
| return token.access_token; |
| } |
| if (this._retriesLeft > 0) { |
| this._retriesLeft--; |
| this._cachedTokenPromise = null; |
| return this._getAccessToken(); |
| } |
| // Fall back to anonymous access. |
| return null; |
| }); |
| } |
| |
| _fetchWithAccessToken(url, options, accessToken) { |
| const params = []; |
| |
| if (accessToken) { |
| params.push(`access_token=${accessToken}`); |
| const baseUrl = this.baseUrl; |
| const pathname = baseUrl ? |
| url.substring(url.indexOf(baseUrl) + baseUrl.length) : url; |
| if (!pathname.startsWith('/a/')) { |
| url = url.replace(pathname, '/a' + pathname); |
| } |
| } |
| |
| const method = options.method || 'GET'; |
| let contentType = options.headers.get('Content-Type'); |
| |
| // For all requests with body, ensure json content type. |
| if (!contentType && options.body) { |
| contentType = 'application/json'; |
| } |
| |
| if (method !== 'GET') { |
| options.method = 'POST'; |
| params.push(`$m=${method}`); |
| // If a request is not GET, and does not have a body, ensure text/plain |
| // content type. |
| if (!contentType) { |
| contentType = 'text/plain'; |
| } |
| } |
| |
| if (contentType) { |
| options.headers.set('Content-Type', 'text/plain'); |
| params.push(`$ct=${encodeURIComponent(contentType)}`); |
| } |
| |
| if (params.length) { |
| url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&'); |
| } |
| return fetch(url, options); |
| } |
| } |
| |
| Auth.TYPE = { |
| XSRF_TOKEN: 'xsrf_token', |
| ACCESS_TOKEN: 'access_token', |
| }; |
| |
| /** @enum {number} */ |
| Auth.STATUS = { |
| UNDETERMINED: 0, |
| AUTHED: 1, |
| NOT_AUTHED: 2, |
| ERROR: 3, |
| }; |
| |
| Auth.CREDS_EXPIRED_MSG = 'Credentails expired.'; |
| |
| // TODO(taoalpha): this whole thing should be moved to a service |
| window.Auth = Auth; |
| Gerrit.Auth = new Auth(); |
| })(window); |