@ydbjs/auth
Version:
Authentication providers for YDB: static credentials, tokens, anonymous, and cloud metadata. Integrates with the core driver for secure access.
118 lines • 5.49 kB
JavaScript
import { channel as dc, tracingChannel } from 'node:diagnostics_channel';
import { loggers } from '@ydbjs/debug';
import { retry } from '@ydbjs/retry';
import { backoff } from '@ydbjs/retry/strategy';
import { CredentialsProvider } from './index.js';
let authTokenFetchCh = tracingChannel('tracing:ydb:auth.token.fetch');
let dbg = loggers.auth.extend('metadata');
/**
* A credentials provider that retrieves tokens from a metadata service.
*
* This class extends the `CredentialsProvider` class and implements the `getToken` method
* to fetch tokens from a specified metadata endpoint. It supports optional retry logic
* and allows customization of the metadata flavor and endpoint.
*
* @extends CredentialsProvider
*/
export class MetadataCredentialsProvider extends CredentialsProvider {
#promise = null;
#token = null;
#flavor = 'Google';
#endpoint = 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token';
// See StaticCredentialsProvider — fire `token.expired` at most once per
// incident; reset on every successful refresh.
#expiredReported = false;
/**
* Creates an instance of `MetadataCredentialsProvider`.
*
* @param credentials - An optional object containing metadata credentials.
* @param credentials.flavor - The metadata flavor (default: 'Google').
* @param credentials.endpoint - The metadata endpoint URL (default: 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token').
*/
constructor(credentials = {}) {
super();
if (credentials.flavor) {
this.#flavor = credentials.flavor;
}
if (credentials.endpoint) {
this.#endpoint = credentials.endpoint;
}
dbg.log('creating metadata credentials provider with flavor: %s, endpoint: %s', this.#flavor, this.#endpoint);
}
/**
* Retrieves an authentication token from the specified endpoint.
* If a valid token is already available and `force` is not true, it returns the cached token.
* Otherwise, it fetches a new token with optional retry logic based on the provided configuration.
*
* @param force - A flag indicating whether to force fetching a new token regardless of the existing one's validity.
* @param signal - An AbortSignal to cancel the operation if needed.
* @returns A promise resolving to the authentication token as a string.
* @throws Will throw an error if the token fetch fails, the response is not OK, or the content type is incorrect.
*/
async getToken(force, signal) {
if (!force && this.#token && this.#token.expired_at > Date.now()) {
dbg.log('returning cached token, expires in %d ms', this.#token.expired_at - Date.now());
return this.#token.value;
}
if (this.#token && this.#token.expired_at <= Date.now() && !this.#expiredReported) {
this.#expiredReported = true;
let stalenessMs = Date.now() - this.#token.expired_at;
dc('ydb:auth.token.expired').publish({ provider: 'metadata', stalenessMs });
}
if (this.#promise) {
dbg.log('token fetch already in progress, waiting for result');
return this.#promise;
}
dbg.log('fetching new token from metadata service');
let retryConfig = {
retry: (err) => err instanceof Error,
signal,
budget: 5,
strategy: backoff(10, 1000),
onRetry: (ctx) => {
dbg.log('retrying token fetch, attempt %d, error: %O', ctx.attempt, ctx.error);
},
};
this.#promise = authTokenFetchCh
.tracePromise(() => retry(retryConfig, (attemptSignal) => this.#fetchTokenAttempt(attemptSignal)), { provider: 'metadata' })
.catch((error) => {
dc('ydb:auth.provider.failed').publish({ provider: 'metadata', error });
throw error;
})
.finally(() => {
this.#promise = null;
});
return this.#promise;
}
async #fetchTokenAttempt(signal) {
dbg.log('attempting to fetch token from %s', this.#endpoint);
let response = await fetch(this.#endpoint, {
headers: { 'Metadata-Flavor': this.#flavor },
signal,
});
dbg.log('%s %s %s', this.#endpoint, response.status, response.headers.get('Content-Type'));
if (!response.ok) {
let error = new Error(`Failed to fetch token: ${response.status} ${response.statusText}`);
dbg.log('error fetching token: %O', error);
throw error;
}
let token = JSON.parse(await response.text());
if (!token.access_token) {
dbg.log('missing access token in response, response: %O', token);
throw new Error('No access token exists in response');
}
this.#token = {
value: token.access_token,
expired_at: Date.now() + (token.expires_in ?? 3600) * 1000,
};
dbg.log('token fetched successfully, expires in %d seconds', token.expires_in ?? 3600);
// Allow the next expiration incident to be reported.
this.#expiredReported = false;
dc('ydb:auth.token.refreshed').publish({
provider: 'metadata',
expiresAt: this.#token.expired_at,
});
return this.#token.value;
}
}
//# sourceMappingURL=metadata.js.map