Skip to content

Commit 799cd13

Browse files
committed
refactor: add support for secure options in StaticCredentialsProvider
Signed-off-by: Vladislav Polyakov <[email protected]>
1 parent cf1e3a6 commit 799cd13

File tree

3 files changed

+97
-33
lines changed

3 files changed

+97
-33
lines changed

.changeset/ten-carrots-repair.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ydbjs/auth': patch
3+
---
4+
5+
Support secure options for static credentials provider

packages/auth/tests/static.e2e.test.ts renamed to packages/auth/src/static.test.ts

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,65 @@
1-
import { afterEach, describe, expect, inject, test, vi } from 'vitest'
2-
import { createClientFactory } from 'nice-grpc'
3-
4-
import { StaticCredentialsProvider } from '../dist/esm/static.js'
5-
6-
let username = inject('credentialsUsername')
7-
let password = inject('credentialsPassword')
8-
let endpoint = inject('credentialsEndpoint')
1+
import * as crypto from 'node:crypto'
2+
import * as fs from 'node:fs/promises'
3+
import * as os from 'node:os'
4+
import * as path from 'node:path'
5+
6+
import { create } from '@bufbuild/protobuf'
7+
import { anyPack } from '@bufbuild/protobuf/wkt'
8+
import { AuthServiceDefinition, LoginResponseSchema, LoginResultSchema } from '@ydbjs/api/auth'
9+
import { StatusIds_StatusCode } from '@ydbjs/api/operation'
10+
import { type ServiceImplementation, createServer } from 'nice-grpc'
11+
import { afterAll, afterEach, describe, expect, test, vi } from 'vitest'
12+
13+
import { StaticCredentialsProvider } from './static.js'
14+
15+
let AuthServiceTestImpl: ServiceImplementation<typeof AuthServiceDefinition> = {
16+
login: async () => {
17+
return create(LoginResponseSchema, {
18+
operation: {
19+
status: StatusIds_StatusCode.SUCCESS,
20+
result: anyPack(LoginResultSchema, create(LoginResultSchema, {
21+
token: crypto.randomUUID()
22+
})),
23+
}
24+
})
25+
}
26+
}
927

1028
describe('StaticCredentialsProvider', async () => {
1129
let calls = 0
12-
let clientFactory = createClientFactory().use((call, options) => {
30+
31+
let server = createServer().use((call, options) => {
1332
calls++
1433
return call.next(call.request, options)
1534
})
1635

36+
server.add(AuthServiceDefinition, AuthServiceTestImpl)
37+
1738
afterEach(() => {
1839
calls = 0
1940
})
2041

42+
afterAll(async () => {
43+
await server.shutdown()
44+
await fs.rm(socket, { force: true });
45+
})
46+
47+
let socket = path.join(os.tmpdir(), `test-grpc-server-${Date.now()}.sock`);
48+
let endpoint = `unix:${socket}`
49+
let username = 'test'
50+
let password = '1234'
51+
52+
await server.listen(endpoint)
53+
2154
test('valid token', async () => {
22-
let provider = new StaticCredentialsProvider({ username, password }, endpoint, clientFactory)
55+
let provider = new StaticCredentialsProvider({ username, password }, endpoint)
2356

2457
let token = await provider.getToken(false)
2558
expect(token, 'Token is not empty').not.empty
2659
})
2760

2861
test('reuse token', async () => {
29-
let provider = new StaticCredentialsProvider({ username, password }, endpoint, clientFactory)
62+
let provider = new StaticCredentialsProvider({ username, password }, endpoint)
3063

3164
let token = await provider.getToken(false)
3265
let token2 = await provider.getToken(false)
@@ -36,7 +69,7 @@ describe('StaticCredentialsProvider', async () => {
3669
})
3770

3871
test('force refresh token', async () => {
39-
let provider = new StaticCredentialsProvider({ username, password }, endpoint, clientFactory)
72+
let provider = new StaticCredentialsProvider({ username, password }, endpoint)
4073

4174
let token = await provider.getToken(false)
4275
let token2 = await provider.getToken(true)
@@ -46,7 +79,7 @@ describe('StaticCredentialsProvider', async () => {
4679
})
4780

4881
test('auto refresh expired token', async () => {
49-
let provider = new StaticCredentialsProvider({ username, password }, endpoint, clientFactory)
82+
let provider = new StaticCredentialsProvider({ username, password }, endpoint)
5083

5184
let token = await provider.getToken(false)
5285
vi.useFakeTimers({ now: new Date(2100, 0, 1) })
@@ -58,7 +91,7 @@ describe('StaticCredentialsProvider', async () => {
5891
})
5992

6093
test('multiple token aquisition', async () => {
61-
let provider = new StaticCredentialsProvider({ username, password }, endpoint, clientFactory)
94+
let provider = new StaticCredentialsProvider({ username, password }, endpoint)
6295

6396
let tokens = await Promise.all([provider.getToken(false), provider.getToken(false), provider.getToken(false)])
6497

@@ -67,7 +100,7 @@ describe('StaticCredentialsProvider', async () => {
67100
})
68101

69102
test('abort token aquisition', async () => {
70-
let provider = new StaticCredentialsProvider({ username, password }, endpoint, clientFactory)
103+
let provider = new StaticCredentialsProvider({ username, password }, endpoint)
71104

72105
let controller = new AbortController()
73106
controller.abort()
@@ -77,7 +110,7 @@ describe('StaticCredentialsProvider', async () => {
77110
})
78111

79112
test('timeout token aquisition', async () => {
80-
let provider = new StaticCredentialsProvider({ username, password }, endpoint, clientFactory)
113+
let provider = new StaticCredentialsProvider({ username, password }, endpoint)
81114

82115
let token = provider.getToken(false, AbortSignal.timeout(0))
83116

packages/auth/src/static.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import * as tls from 'node:tls'
2+
13
import { anyUnpack } from '@bufbuild/protobuf/wkt'
4+
import { type ChannelOptions, credentials } from '@grpc/grpc-js'
25
import { AuthServiceDefinition, LoginResultSchema } from '@ydbjs/api/auth'
36
import { StatusIds_StatusCode } from '@ydbjs/api/operation'
4-
import { type Client, ClientError, type ClientFactory, Status, createChannel, createClientFactory } from 'nice-grpc'
7+
import { YDBError } from '@ydbjs/error'
8+
import { defaultRetryConfig, retry } from '@ydbjs/retry'
9+
import { type Client, ClientError, Status, createChannel, createClient } from 'nice-grpc'
510

611
import { CredentialsProvider } from './index.js'
7-
import { defaultRetryConfig, retry } from '@ydbjs/retry'
8-
import { YDBError } from '@ydbjs/error'
912

1013
export type StaticCredentialsToken = {
1114
value: string
@@ -37,16 +40,28 @@ export class StaticCredentialsProvider extends CredentialsProvider {
3740
#password: string
3841

3942
constructor(
40-
credentials: StaticCredentials,
43+
{ username, password }: StaticCredentials,
4144
endpoint: string,
42-
clientFactory: ClientFactory = createClientFactory()
45+
secureOptions?: tls.SecureContextOptions | undefined,
46+
channelOptions?: ChannelOptions
4347
) {
4448
super()
45-
this.#username = credentials.username
46-
this.#password = credentials.password
49+
this.#username = username
50+
this.#password = password
51+
52+
let cs = new URL(endpoint)
4753

48-
let cs = new URL(endpoint.replace(/^grpc/, 'http'))
49-
this.#client = clientFactory.create(AuthServiceDefinition, createChannel(cs.origin))
54+
if (['unix:', 'http:', 'https:', 'grpc:', 'grpcs:'].includes(cs.protocol) === false) {
55+
throw new Error('Invalid connection string protocol. Must be one of unix, grpc, grpcs, http, https')
56+
}
57+
58+
let address = `${cs.protocol}//${cs.host}${cs.pathname}`
59+
60+
let channelCredentials = secureOptions ?
61+
credentials.createFromSecureContext(tls.createSecureContext(secureOptions)) :
62+
credentials.createInsecure()
63+
64+
this.#client = createClient(AuthServiceDefinition, createChannel(address, channelCredentials, channelOptions))
5065
}
5166

5267
/**
@@ -66,7 +81,6 @@ export class StaticCredentialsProvider extends CredentialsProvider {
6681

6782
this.#promise = retry({ ...defaultRetryConfig, signal, idempotent: true }, async () => {
6883
let response = await this.#client.login({ user: this.#username, password: this.#password }, { signal })
69-
7084
if (!response.operation) {
7185
throw new ClientError(AuthServiceDefinition.login.path, Status.UNKNOWN, 'No operation in response')
7286
}
@@ -80,13 +94,25 @@ export class StaticCredentialsProvider extends CredentialsProvider {
8094
throw new ClientError(AuthServiceDefinition.login.path, Status.UNKNOWN, 'No result in operation')
8195
}
8296

83-
// Parse the JWT token to extract expiration time
84-
const [, payload] = result.token.split('.')
85-
const decodedPayload = JSON.parse(Buffer.from(payload, 'base64').toString())
86-
87-
this.#token = {
88-
value: result.token,
89-
...decodedPayload,
97+
// The result.token is a JWT in the format header.payload.signature.
98+
// We attempt to decode the payload to extract token metadata (aud, exp, iat, sub).
99+
// If the token is not in the expected format, we fallback to default values.
100+
let [header, payload, signature] = result.token.split('.')
101+
if (header && payload && signature) {
102+
let decodedPayload = JSON.parse(Buffer.from(payload, 'base64').toString())
103+
104+
this.#token = {
105+
value: result.token,
106+
...decodedPayload,
107+
}
108+
} else {
109+
this.#token = {
110+
value: result.token,
111+
aud: [],
112+
exp: Math.floor(Date.now() / 1000) + 5 * 60, // fallback: 5 minutes from now
113+
iat: Math.floor(Date.now() / 1000),
114+
sub: '',
115+
}
90116
}
91117

92118
return this.#token!.value!

0 commit comments

Comments
 (0)