blob: 7570ba3fc1334c80cbb91dad08448af75d7f699b [file] [log] [blame]
/**
* @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;
});
}
}