Skip to content

Commit a6b8000

Browse files
committed
fix authentication mechanism
1 parent 82a3b01 commit a6b8000

File tree

4 files changed

+100
-43
lines changed

4 files changed

+100
-43
lines changed

src/auth/index.ts

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,25 @@ import {
66
ServiceAccountAuth,
77
UsernamePasswordAuth
88
} from "../types";
9-
import { TokenKey, inMemoryCache, noneCache } from "../common/tokenCache";
10-
import ReadWriteLock from "rwlock";
9+
import {
10+
TokenKey,
11+
inMemoryCache,
12+
noneCache,
13+
rwLock
14+
} from "../common/tokenCache";
1115

1216
type Login = {
1317
access_token: string;
1418
token_type: string;
1519
expires_in: number;
1620
};
1721

22+
type TokenInfo = {
23+
access_token: string;
24+
// seconds until expiration
25+
expires_in: number;
26+
};
27+
1828
const AUTH_AUDIENCE = "https://api.firebolt.io";
1929
const AUTH_GRANT_TYPE = "client_credentials";
2030

@@ -23,7 +33,7 @@ export class Authenticator {
2333
options: ConnectionOptions;
2434

2535
accessToken?: string;
26-
private readonly rwlock = new ReadWriteLock();
36+
tokenExpirationTime?: number;
2737

2838
constructor(context: Context, options: ConnectionOptions) {
2939
context.httpClient.authenticator = this;
@@ -62,23 +72,40 @@ export class Authenticator {
6272
}
6373

6474
private setToken(token: string, expiresIn: number) {
75+
// Set expiration to half of the expiresIn value
76+
// to allow for some buffer time before the token expires
77+
const expirationTimeMs = Date.now() + (expiresIn * 1000) / 2;
6578
this.accessToken = token;
79+
this.tokenExpirationTime = expirationTimeMs;
80+
// Update cache
6681
const key = this.getCacheKey();
67-
key && this.getCache().set(key, { token, expiration: expiresIn });
82+
key &&
83+
this.getCache().set(key, { token, tokenExpiryTimeMs: expirationTimeMs });
6884
}
6985

70-
private getCachedToken(): string | undefined {
86+
private getCachedTokenInfo():
87+
| { token: string; tokenExpiryTimeMs: number }
88+
| undefined {
7189
const key = this.getCacheKey();
72-
return key ? this.getCache().get(key)?.token : undefined;
73-
}
90+
if (!key) return undefined;
7491

75-
getHeaders() {
76-
if (this.accessToken) {
92+
const cachedTokenInfo = this.getCache().get(key);
93+
// Check if token exists and is not expired
94+
if (cachedTokenInfo && Date.now() < cachedTokenInfo.tokenExpiryTimeMs) {
7795
return {
78-
Authorization: `Bearer ${this.accessToken}`
96+
token: cachedTokenInfo.token,
97+
tokenExpiryTimeMs: cachedTokenInfo.tokenExpiryTimeMs
7998
};
8099
}
81-
return {};
100+
101+
return undefined;
102+
}
103+
104+
async getToken(): Promise<string | undefined> {
105+
if (this.tokenExpirationTime && this.tokenExpirationTime < Date.now()) {
106+
await this.authenticate();
107+
}
108+
return this.accessToken;
82109
}
83110

84111
private static getAuthEndpoint(apiEndpoint: string) {
@@ -94,7 +121,9 @@ export class Authenticator {
94121
return myURL.toString();
95122
}
96123

97-
private async authenticateUsernamePassword(auth: UsernamePasswordAuth) {
124+
private async authenticateUsernamePassword(
125+
auth: UsernamePasswordAuth
126+
): Promise<TokenInfo> {
98127
const { httpClient, apiEndpoint } = this.context;
99128
const { username, password } = auth;
100129
const url = `${apiEndpoint}/${USERNAME_PASSWORD_LOGIN}`;
@@ -103,19 +132,21 @@ export class Authenticator {
103132
password
104133
});
105134

106-
this.accessToken = undefined;
107-
135+
// Expiration is in seconds
108136
const { access_token, expires_in } = await httpClient
109137
.request<Login>("POST", url, {
110138
body,
111-
retry: false
139+
retry: false,
140+
auth: false
112141
})
113142
.ready();
114143

115-
this.setToken(access_token, expires_in);
144+
return { access_token, expires_in };
116145
}
117146

118-
private async authenticateServiceAccount(auth: ServiceAccountAuth) {
147+
private async authenticateServiceAccount(
148+
auth: ServiceAccountAuth
149+
): Promise<TokenInfo> {
119150
const { httpClient, apiEndpoint } = this.context;
120151
const { client_id, client_secret } = auth;
121152

@@ -128,19 +159,19 @@ export class Authenticator {
128159
});
129160
const url = `${authEndpoint}${SERVICE_ACCOUNT_LOGIN}`;
130161

131-
this.accessToken = undefined;
132-
162+
// Expiration is in seconds
133163
const { access_token, expires_in } = await httpClient
134164
.request<Login>("POST", url, {
135165
retry: false,
136166
headers: {
137167
"Content-Type": "application/x-www-form-urlencoded"
138168
},
139-
body: params
169+
body: params,
170+
auth: false
140171
})
141172
.ready();
142173

143-
this.setToken(access_token, expires_in);
174+
return { access_token, expires_in };
144175
}
145176

146177
isUsernamePassword() {
@@ -163,19 +194,21 @@ export class Authenticator {
163194
// Try to get token from cache using read lock
164195
const cachedToken = await this.tryGetCachedToken();
165196
if (cachedToken) {
166-
this.accessToken = cachedToken;
197+
this.accessToken = cachedToken.token;
198+
this.tokenExpirationTime = cachedToken.tokenExpiryTimeMs;
167199
return;
168200
}
169-
170201
// No cached token, acquire write lock and authenticate
171202
await this.acquireWriteLockAndAuthenticate();
172203
}
173204

174-
private async tryGetCachedToken(): Promise<string | undefined> {
205+
private async tryGetCachedToken(): Promise<
206+
{ token: string; tokenExpiryTimeMs: number } | undefined
207+
> {
175208
return new Promise((resolve, reject) => {
176-
this.rwlock.readLock(releaseReadLock => {
209+
rwLock.readLock(releaseReadLock => {
177210
try {
178-
const cachedToken = this.getCachedToken();
211+
const cachedToken = this.getCachedTokenInfo();
179212
resolve(cachedToken);
180213
} catch (error) {
181214
reject(error instanceof Error ? error : new Error(String(error)));
@@ -188,15 +221,15 @@ export class Authenticator {
188221

189222
private async acquireWriteLockAndAuthenticate(): Promise<void> {
190223
return new Promise((resolve, reject) => {
191-
this.rwlock.writeLock(async releaseWriteLock => {
224+
rwLock.writeLock(async releaseWriteLock => {
192225
try {
193226
// Double-check cache in case another thread authenticated while waiting
194-
const cachedToken = this.getCachedToken();
195-
if (cachedToken) {
196-
this.accessToken = cachedToken;
227+
const cachedTokenInfo = this.getCachedTokenInfo();
228+
if (cachedTokenInfo) {
229+
this.accessToken = cachedTokenInfo.token;
230+
this.tokenExpirationTime = cachedTokenInfo.tokenExpiryTimeMs;
197231
return resolve();
198232
}
199-
200233
await this.performAuthentication();
201234

202235
resolve();
@@ -212,14 +245,20 @@ export class Authenticator {
212245
private async performAuthentication(): Promise<void> {
213246
const options = this.options.auth || this.options;
214247

215-
if (this.isUsernamePassword()) {
216-
return this.authenticateUsernamePassword(options as UsernamePasswordAuth);
217-
}
248+
let auth: TokenInfo;
218249

219-
if (this.isServiceAccount()) {
220-
return this.authenticateServiceAccount(options as ServiceAccountAuth);
250+
if (this.isUsernamePassword()) {
251+
auth = await this.authenticateUsernamePassword(
252+
options as UsernamePasswordAuth
253+
);
254+
} else if (this.isServiceAccount()) {
255+
auth = await this.authenticateServiceAccount(
256+
options as ServiceAccountAuth
257+
);
258+
} else {
259+
throw new Error("Please provide valid auth credentials");
221260
}
222261

223-
throw new Error("Please provide valid auth credentials");
262+
this.setToken(auth.access_token, auth.expires_in);
224263
}
225264
}

src/common/tokenCache.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ReadWriteLock from "rwlock";
12
import { AccountInfo } from "../connection/connection_v1";
23

34
export type TokenKey = {
@@ -8,7 +9,7 @@ export type TokenKey = {
89

910
export type TokenRecord = {
1011
token: string;
11-
expiration: number;
12+
tokenExpiryTimeMs: number;
1213
};
1314

1415
export type AccountKey = {
@@ -62,7 +63,6 @@ export class InMemoryCacheStorage<KeyType, RecordType> {
6263
const lookup = this.makeLookupString(key);
6364
const record = this.storage[lookup];
6465
if (!this.isValidRecord(record)) {
65-
this.clear(key);
6666
return null;
6767
}
6868
return record;
@@ -84,11 +84,12 @@ export class InMemoryTokenCacheStorage extends InMemoryCacheStorage<
8484
TokenRecord
8585
> {
8686
protected isValidRecord(record: TokenRecord | undefined): boolean {
87-
return typeof record != "undefined" && Date.now() < record.expiration;
87+
return (
88+
typeof record != "undefined" && Date.now() < record.tokenExpiryTimeMs
89+
);
8890
}
8991

9092
protected modifyRecord(record: TokenRecord): TokenRecord {
91-
record.expiration = Date.now() + record.expiration;
9293
return record;
9394
}
9495
}
@@ -116,3 +117,4 @@ export class InMemoryCache implements Cache {
116117

117118
export const inMemoryCache = new InMemoryCache();
118119
export const noneCache = new NoneCache();
120+
export const rwLock = new ReadWriteLock();

src/http/node.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type RequestOptions = {
1414
body?: string;
1515
raw?: boolean;
1616
retry?: boolean;
17+
auth?: boolean;
1718
};
1819

1920
type ErrorResponse = {
@@ -84,8 +85,17 @@ export class NodeHttpClient {
8485
};
8586

8687
const makeRequest = async () => {
87-
if (this.authenticator) {
88-
const authHeaders = await this.authenticator.getHeaders();
88+
// No authentication for auth request
89+
if (options?.auth !== false) {
90+
const token = await this.authenticator.getToken();
91+
if (!token) {
92+
throw new AuthenticationError({
93+
message: "Failed to get the access token when making a request."
94+
});
95+
}
96+
const authHeaders = {
97+
Authorization: `Bearer ${token}`
98+
};
8999
Object.assign(headers, authHeaders);
90100
}
91101

@@ -107,6 +117,9 @@ export class NodeHttpClient {
107117

108118
if (response.status === 401 && retry) {
109119
try {
120+
console.warn(
121+
"Access token expired (401), refreshing access token and retrying request"
122+
);
110123
this.authenticator.clearCache();
111124
await this.authenticator.authenticate();
112125
} catch (error) {

test/unit/http.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const authHandlerV1 = rest.post(
3131
}
3232
);
3333

34+
// Increase Jest timeout for tests that might involve timeouts
35+
jest.setTimeout(10000);
36+
3437
describe.each([
3538
["v1", authHandlerV1, { username: "user", password: "fake_password" }],
3639
["v2", authHandlerV2, { client_id: "user", client_secret: "fake_password" }]

0 commit comments

Comments
 (0)