Skip to content

Commit 444a34d

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

File tree

1 file changed

+98
-42
lines changed

1 file changed

+98
-42
lines changed
Lines changed: 98 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,53 @@
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 { getOperationPayload } from "../utils/process-ydb-operation-result";
87
import * as grpc from "@grpc/grpc-js";
9-
import {addCredentialsToMetadata} from "./add-credentials-to-metadata";
8+
import { addCredentialsToMetadata } from "./add-credentials-to-metadata";
109

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";
10+
import { IAuthService } from "./i-auth-service";
11+
import { HasLogger } from "../logger/has-logger";
12+
import { Logger } from "../logger/simple-logger";
13+
import { getDefaultLogger } from "../logger/get-default-logger";
1514

15+
/**
16+
* Static credentials token.
17+
*/
18+
export type StaticCredentialsToken = {
19+
value: string
20+
aud: string[]
21+
exp: number
22+
iat: number
23+
sub: string
24+
}
25+
26+
/**
27+
* Interface for options used in static credentials authentication.
28+
*/
1629
interface StaticCredentialsAuthOptions {
17-
/** Custom ssl sertificates. If you use it in driver, you must use it here too */
30+
/** Custom SSL certificates. If you use it in driver, you must use it here too */
1831
sslCredentials?: ISslCredentials;
32+
1933
/**
2034
* Timeout for token request in milliseconds
2135
* @default 10 * 1000
2236
*/
2337
tokenRequestTimeout?: number;
24-
/** Expiration time for token in milliseconds
38+
39+
/**
40+
* Expiration time for token in milliseconds
41+
* @deprecated Use tokenRefreshInterval instead
2542
* @default 6 * 60 * 60 * 1000
2643
*/
27-
tokenExpirationTimeout?: number
44+
tokenExpirationTimeout?: number;
45+
46+
/**
47+
* Time interval in milliseconds after which the token will be refreshed.
48+
* When specified, token refresh is based on this timer rather than the token's exp field.
49+
*/
50+
tokenRefreshInterval?: number;
2851
}
2952

3053
class StaticCredentialsGrpcService extends GrpcService<Ydb.Auth.V1.AuthService> implements HasLogger {
@@ -44,16 +67,19 @@ class StaticCredentialsGrpcService extends GrpcService<Ydb.Auth.V1.AuthService>
4467

4568
export class StaticCredentialsAuthService implements IAuthService {
4669
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;
70+
private readonly tokenRefreshInterval: number | null = null;
71+
72+
private readonly user: string;
73+
private readonly password: string;
74+
private readonly endpoint: string;
75+
private readonly sslCredentials: ISslCredentials | undefined;
76+
5577
public readonly logger: Logger;
5678

79+
private token: StaticCredentialsToken | null = null;
80+
// Mutex
81+
private promise: Promise<grpc.Metadata> | null = null;
82+
5783
constructor(
5884
user: string,
5985
password: string,
@@ -74,25 +100,37 @@ export class StaticCredentialsAuthService implements IAuthService {
74100
loggerOrOptions?: Logger | StaticCredentialsAuthOptions,
75101
options?: StaticCredentialsAuthOptions
76102
) {
77-
this.tokenTimestamp = null;
78103
this.user = user;
79104
this.password = password;
80105
this.endpoint = endpoint;
81106
this.sslCredentials = options?.sslCredentials;
107+
82108
if (typeof loggerOrOptions === 'object' && loggerOrOptions !== null && 'error' in loggerOrOptions) {
83109
this.logger = loggerOrOptions as Logger;
84110
} else {
85111
options = loggerOrOptions;
86112
this.logger = getDefaultLogger();
87113
}
114+
88115
if (options?.tokenRequestTimeout) this.tokenRequestTimeout = options.tokenRequestTimeout;
89-
if (options?.tokenExpirationTimeout) this.tokenExpirationTimeout = options.tokenExpirationTimeout;
90-
}
116+
if (options?.tokenExpirationTimeout) this.tokenRefreshInterval = options.tokenExpirationTimeout;
117+
if (options?.tokenRefreshInterval) this.tokenRefreshInterval = options.tokenRefreshInterval;
91118

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

98136
private async sendTokenRequest(): Promise<AuthServiceResult> {
@@ -101,34 +139,52 @@ export class StaticCredentialsAuthService implements IAuthService {
101139
this.sslCredentials,
102140
this.logger,
103141
);
142+
104143
const tokenPromise = runtimeAuthService.login({
105144
user: this.user,
106145
password: this.password,
107146
});
147+
108148
const response = await withTimeout(tokenPromise, this.tokenRequestTimeout);
109149
const result = AuthServiceResult.decode(getOperationPayload(response));
110150
runtimeAuthService.destroy();
151+
111152
return result;
112153
}
113154

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

124173
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;
174+
if (this.token && this.token.exp > Date.now() / 1000) {
175+
return addCredentialsToMetadata(this.token.value)
131176
}
132-
return addCredentialsToMetadata(this.token);
177+
178+
if (this.promise) {
179+
return this.promise;
180+
}
181+
182+
this.promise = this.updateToken()
183+
.then(token => addCredentialsToMetadata(token.value))
184+
.finally(() => {
185+
this.promise = null;
186+
})
187+
188+
return this.promise
133189
}
134190
}

0 commit comments

Comments
 (0)