@authx/http-proxy-resource
Version:
The AuthX proxy for resources is a flexible HTTP proxy designed to sit in front of a resource.
112 lines • 4.09 kB
JavaScript
import { EventEmitter } from "events";
class NoTokenError extends Error {
}
class TokenDataCacheEntry {
basicToken;
conf;
errorHandler;
lastRefreshRequestTime;
lastValidRefreshTime = null;
forceRefresh = false;
currentToken = null;
nextToken = null;
constructor(basicToken, conf, errorHandler) {
this.basicToken = basicToken;
this.conf = conf;
this.errorHandler = errorHandler;
this.lastRefreshRequestTime = Date.now();
}
get token() {
if (this.currentToken === null) {
this.startFetchNextToken();
this.currentToken = this.nextToken;
}
else if (this.forceRefresh ||
(this.conf.timeSource() - this.lastRefreshRequestTime) / 1_000 >
this.conf.tokenRefreshSeconds) {
this.startFetchNextToken();
}
if (!this.currentToken)
throw new Error("currentToken was unexpectedly null");
return this.currentToken;
}
get secondsSinceLastRefresh() {
return ((this.conf.timeSource() -
(this.lastValidRefreshTime ?? this.lastRefreshRequestTime)) /
1_000);
}
startFetchNextToken() {
this.nextToken = this.fetchNextToken();
this.nextToken.then(() => {
this.currentToken = this.nextToken;
this.lastValidRefreshTime = this.conf.timeSource();
}, (err) => {
if (err instanceof NoTokenError) {
// This error is cachable, so replace the cache with it
this.currentToken = this.nextToken;
this.lastValidRefreshTime = this.conf.timeSource();
}
else {
this.forceRefresh = true;
}
this.errorHandler(err);
});
this.lastRefreshRequestTime = this.conf.timeSource();
}
async fetchNextToken() {
const response = await this.conf.fetchFunc(`${this.conf.authxUrl}/graphql`, {
headers: {
"Content-Type": "application/json",
Authorization: this.basicToken,
},
method: "POST",
body: JSON.stringify({
query: "query { viewer { access id user { id } } }",
}),
});
if (![200, 401].includes(response.status)) {
throw new Error(`Unexpected response code from AuthX ${response.status}`);
}
const responseJson = await response.json();
const ret = responseJson?.data?.viewer;
if (!ret)
throw new NoTokenError("Response did not include a valid token");
if (!Array.isArray(ret?.access))
throw new NoTokenError("Response did not include scopes");
if (!ret?.id)
throw new NoTokenError("Response did not include an authorization id");
if (!ret?.user?.id)
throw new NoTokenError("Response did not include a user id");
return ret;
}
}
export class TokenDataCache extends EventEmitter {
conf;
cache = new Map();
constructor(conf) {
super();
this.conf = conf;
}
getToken(basicToken) {
for (const k of [...this.cache.keys()]) {
const cacheValue = this.cache.get(k);
if (cacheValue &&
cacheValue.secondsSinceLastRefresh > this.conf.tokenExpirySeconds) {
this.cache.delete(k);
}
}
if (!this.cache.has(basicToken)) {
this.cache.set(basicToken, new TokenDataCacheEntry(basicToken, this.conf, (err) => {
// Only trigger the error event on truly unexpected errors. Other cases are simply handled as auth failures
if (!(err instanceof NoTokenError)) {
this.emit("error", err);
}
}));
}
const ret = this.cache.get(basicToken)?.token;
if (!ret)
throw new Error("Unexpectedly unable to find TokenDataCacheEntry, cache is invalid");
return ret;
}
}
//# sourceMappingURL=TokenDataCache.js.map