| /** |
| * @license |
| * Copyright 2017 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import {AuthRequestInit, Finalizable} from '../../types/types'; |
| import {fire} from '../../utils/event-util'; |
| import {getBaseUrl} from '../../utils/url-util'; |
| import {AuthService} from './gr-auth'; |
| |
| const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s |
| |
| const CREDS_EXPIRED_MSG = 'Credentials expired.'; |
| |
| // visible for testing |
| export enum AuthStatus { |
| UNDETERMINED = 0, |
| AUTHED = 1, |
| NOT_AUTHED = 2, |
| ERROR = 3, |
| } |
| |
| interface AuthRequestInitWithHeaders extends AuthRequestInit { |
| // RequestInit define headers as optional property with a type |
| // Headers | string[][] | Record<string, string> |
| // In Auth class headers property is always set and has type Headers |
| headers: Headers; |
| } |
| |
| /** |
| * Auth class. |
| */ |
| export class Auth implements AuthService, Finalizable { |
| private authCheckPromise?: Promise<boolean>; |
| |
| private _last_auth_check_time: number = Date.now(); |
| |
| private _status = AuthStatus.UNDETERMINED; |
| |
| finalize() {} |
| |
| /** |
| * Returns if user is authed or not. |
| */ |
| authCheck(): Promise<boolean> { |
| if ( |
| !this.authCheckPromise || |
| Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS |
| ) { |
| // Refetch after last check expired |
| this.authCheckPromise = fetch(`${getBaseUrl()}/auth-check`) |
| .then(res => { |
| // Make a call that requires loading the body of the request. This makes it so that the browser |
| // can close the request even though callers of this method might only ever read headers. |
| // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome |
| try { |
| res.clone().text(); |
| } catch { |
| // Ignore error |
| } |
| |
| // auth-check will return 204 if authed |
| // treat the rest as unauthed |
| if (res.status === 204) { |
| this._setStatus(AuthStatus.AUTHED); |
| return true; |
| } else { |
| this._setStatus(AuthStatus.NOT_AUTHED); |
| return false; |
| } |
| }) |
| .catch(() => { |
| this._setStatus(AuthStatus.ERROR); |
| // Reset authCheckPromise to avoid caching the failed promise |
| this.authCheckPromise = undefined; |
| return false; |
| }); |
| this._last_auth_check_time = Date.now(); |
| } |
| |
| return this.authCheckPromise; |
| } |
| |
| clearCache() { |
| this.authCheckPromise = undefined; |
| } |
| |
| private _setStatus(status: AuthStatus) { |
| if (this._status === status) return; |
| |
| if (this._status === AuthStatus.AUTHED) { |
| fire(document, 'auth-error', { |
| message: CREDS_EXPIRED_MSG, |
| action: 'Refresh credentials', |
| }); |
| } |
| this._status = status; |
| } |
| |
| // visible for testing |
| get status() { |
| return this._status; |
| } |
| |
| get isAuthed() { |
| return this._status === AuthStatus.AUTHED; |
| } |
| |
| /** |
| * Perform network fetch with authentication. |
| */ |
| fetch(url: string, options?: AuthRequestInit): Promise<Response> { |
| const optionsWithHeaders: AuthRequestInitWithHeaders = { |
| headers: new Headers(), |
| ...options, |
| }; |
| return this._fetchWithXsrfToken(url, optionsWithHeaders); |
| } |
| |
| // private but used in test |
| _getCookie(name: string): string { |
| 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 false; |
| }); |
| return result; |
| } |
| |
| private _fetchWithXsrfToken( |
| url: string, |
| options: AuthRequestInitWithHeaders |
| ): Promise<Response> { |
| 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 this._ensureBodyLoaded(fetch(url, options)); |
| } |
| |
| private _ensureBodyLoaded(response: Promise<Response>): Promise<Response> { |
| return response.then(response => { |
| if (!response.ok) { |
| // Make a call that requires loading the body of the request. This makes it so that the browser |
| // can close the request even though callers of this method might only ever read headers. |
| // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome |
| try { |
| response.clone().text(); |
| } catch { |
| // Ignore error |
| } |
| } |
| return response; |
| }); |
| } |
| } |