Skip to content

Commit 02136a1

Browse files
committed
feat: enhance static credentials authentication with token refresh interval and improved token handling
1 parent ca082be commit 02136a1

File tree

1 file changed

+99
-42
lines changed

1 file changed

+99
-42
lines changed
Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,54 @@
1-
import {Ydb} from "ydb-sdk-proto";
1+
import { Ydb } from "ydb-sdk-proto";
22
import AuthServiceResult = Ydb.Auth.LoginResult;
3-
import {ISslCredentials} from "../utils/ssl-credentials";
4-
import {GrpcService, withTimeout} from "../utils";
5-
import {retryable} from "../retries_obsoleted";
6-
import {DateTime} from "luxon";
7-
import {getOperationPayload} from "../utils/process-ydb-operation-result";
3+
import { ISslCredentials } from "../utils/ssl-credentials";
4+
import { GrpcService, withTimeout } from "../utils";
5+
import { retryable } from "../retries_obsoleted";
6+
import { DateTime } from "luxon";
7+
import { getOperationPayload } from "../utils/process-ydb-operation-result";
88
import * as grpc from "@grpc/grpc-js";
9-
import {addCredentialsToMetadata} from "./add-credentials-to-metadata";
9+
import { addCredentialsToMetadata } from "./add-credentials-to-metadata";
1010

11-
import {IAuthService} from "./i-auth-service";
12-
import {HasLogger} from "../logger/has-logger";
13-
import {Logger} from "../logger/simple-logger";
14-
import {getDefaultLogger} from "../logger/get-default-logger";
11+
import { IAuthService } from "./i-auth-service";
12+
import { HasLogger } from "../logger/has-logger";
13+
import { Logger } from "../logger/simple-logger";
14+
import { getDefaultLogger } from "../logger/get-default-logger";
1515

16+
/**
17+
* Static credentials token.
18+
*/
19+
export type StaticCredentialsToken = {
20+
value: string
21+
aud: string[]
22+
exp: number
23+
iat: number
24+
sub: string
25+
}
26+
27+
/**
28+
* Interface for options used in static credentials authentication.
29+
*/
1630
interface StaticCredentialsAuthOptions {
17-
/** Custom ssl sertificates. If you use it in driver, you must use it here too */
31+
/** Custom SSL certificates. If you use it in driver, you must use it here too */
1832
sslCredentials?: ISslCredentials;
33+
1934
/**
2035
* Timeout for token request in milliseconds
2136
* @default 10 * 1000
2237
*/
2338
tokenRequestTimeout?: number;
24-
/** Expiration time for token in milliseconds
39+
40+
/**
41+
* Expiration time for token in milliseconds
42+
* @deprecated Use tokenRefreshInterval instead
2543
* @default 6 * 60 * 60 * 1000
2644
*/
27-
tokenExpirationTimeout?: number
45+
tokenExpirationTimeout?: number;
46+
47+
/**
48+
* Time interval in milliseconds after which the token will be refreshed.
49+
* When specified, token refresh is based on this timer rather than the token's exp field.
50+
*/
51+
tokenRefreshInterval?: number;
2852
}
2953

3054
class StaticCredentialsGrpcService extends GrpcService<Ydb.Auth.V1.AuthService> implements HasLogger {
@@ -44,16 +68,19 @@ class StaticCredentialsGrpcService extends GrpcService<Ydb.Auth.V1.AuthService>
4468

4569
export class StaticCredentialsAuthService implements IAuthService {
4670
private readonly tokenRequestTimeout = 10 * 1000;
47-
private readonly tokenExpirationTimeout = 6 * 60 * 60 * 1000;
48-
private tokenTimestamp: DateTime | null;
49-
private token: string = '';
50-
private tokenUpdatePromise: Promise<any> | null = null;
51-
private user: string;
52-
private password: string;
53-
private endpoint: string;
54-
private sslCredentials: ISslCredentials | undefined;
71+
private readonly tokenRefreshInterval: number | null = null;
72+
73+
private readonly user: string;
74+
private readonly password: string;
75+
private readonly endpoint: string;
76+
private readonly sslCredentials: ISslCredentials | undefined;
77+
5578
public readonly logger: Logger;
5679

80+
private token: StaticCredentialsToken | null = null;
81+
// Mutex
82+
private promise: Promise<grpc.Metadata> | null = null;
83+
5784
constructor(
5885
user: string,
5986
password: string,
@@ -74,25 +101,37 @@ export class StaticCredentialsAuthService implements IAuthService {
74101
loggerOrOptions?: Logger | StaticCredentialsAuthOptions,
75102
options?: StaticCredentialsAuthOptions
76103
) {
77-
this.tokenTimestamp = null;
78104
this.user = user;
79105
this.password = password;
80106
this.endpoint = endpoint;
81107
this.sslCredentials = options?.sslCredentials;
108+
82109
if (typeof loggerOrOptions === 'object' && loggerOrOptions !== null && 'error' in loggerOrOptions) {
83110
this.logger = loggerOrOptions as Logger;
84111
} else {
85112
options = loggerOrOptions;
86113
this.logger = getDefaultLogger();
87114
}
115+
88116
if (options?.tokenRequestTimeout) this.tokenRequestTimeout = options.tokenRequestTimeout;
89-
if (options?.tokenExpirationTimeout) this.tokenExpirationTimeout = options.tokenExpirationTimeout;
90-
}
117+
if (options?.tokenExpirationTimeout) this.tokenRefreshInterval = options.tokenExpirationTimeout;
118+
if (options?.tokenRefreshInterval) this.tokenRefreshInterval = options.tokenRefreshInterval;
91119

92-
private get expired() {
93-
return !this.tokenTimestamp || (
94-
DateTime.utc().diff(this.tokenTimestamp).valueOf() > this.tokenExpirationTimeout
95-
);
120+
if (this.tokenRefreshInterval) {
121+
let timer = setInterval(() => {
122+
if (this.promise) {
123+
return
124+
}
125+
126+
this.promise = this.updateToken()
127+
.then(token => addCredentialsToMetadata(token.value))
128+
.finally(() => {
129+
this.promise = null;
130+
})
131+
}, this.tokenRefreshInterval);
132+
133+
timer.unref()
134+
}
96135
}
97136

98137
private async sendTokenRequest(): Promise<AuthServiceResult> {
@@ -101,34 +140,52 @@ export class StaticCredentialsAuthService implements IAuthService {
101140
this.sslCredentials,
102141
this.logger,
103142
);
143+
104144
const tokenPromise = runtimeAuthService.login({
105145
user: this.user,
106146
password: this.password,
107147
});
148+
108149
const response = await withTimeout(tokenPromise, this.tokenRequestTimeout);
109150
const result = AuthServiceResult.decode(getOperationPayload(response));
110151
runtimeAuthService.destroy();
152+
111153
return result;
112154
}
113155

114-
private async updateToken() {
115-
const {token} = await this.sendTokenRequest();
116-
if (token) {
117-
this.token = token;
118-
this.tokenTimestamp = DateTime.utc();
119-
} else {
156+
private async updateToken(): Promise<StaticCredentialsToken> {
157+
const { token } = await this.sendTokenRequest();
158+
if (!token) {
120159
throw new Error('Received empty token from static credentials!');
121160
}
161+
162+
// Parse the JWT token to extract expiration time
163+
const [, payload] = token.split('.');
164+
const decodedPayload = JSON.parse(Buffer.from(payload, 'base64').toString());
165+
166+
this.token = {
167+
value: token,
168+
...decodedPayload
169+
};
170+
171+
return this.token!
122172
}
123173

124174
public async getAuthMetadata(): Promise<grpc.Metadata> {
125-
if (this.expired || this.tokenUpdatePromise) {
126-
if (!this.tokenUpdatePromise) {
127-
this.tokenUpdatePromise = this.updateToken();
128-
}
129-
await this.tokenUpdatePromise;
130-
this.tokenUpdatePromise = null;
175+
if (this.token && this.token.exp > Date.now() / 1000) {
176+
return addCredentialsToMetadata(this.token.value)
131177
}
132-
return addCredentialsToMetadata(this.token);
178+
179+
if (this.promise) {
180+
return this.promise;
181+
}
182+
183+
this.promise = this.updateToken()
184+
.then(token => addCredentialsToMetadata(token.value))
185+
.finally(() => {
186+
this.promise = null;
187+
})
188+
189+
return this.promise
133190
}
134191
}

0 commit comments

Comments
 (0)