Skip to content

Commit fce4499

Browse files
committed
Merge latest main into telemetry-3-client-management
2 parents bda2cac + 775e642 commit fce4499

29 files changed

+2184
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release History
22

3+
## 1.13.0
4+
5+
- Add token federation support with custom token providers (databricks/databricks-sql-nodejs#318, databricks/databricks-sql-nodejs#319, databricks/databricks-sql-nodejs#320 by @madhav-db)
6+
- Add metric view metadata support (databricks/databricks-sql-nodejs#312 by @shivam2680)
7+
- Fix: Avoid calling require('lz4') if it's really not required (databricks/databricks-sql-nodejs#316 by @ikkala)
8+
- Add telemetry foundation (off by default) (databricks/databricks-sql-nodejs#324 by @samikshya-db)
9+
310
## 1.12.0
411

512
- Support for session parameters (databricks/databricks-sql-nodejs#307 by @sreekanth-db)

examples/tokenFederation/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Token Federation Examples
2+
3+
Examples demonstrating the token provider and federation features of the Databricks SQL Node.js Driver.
4+
5+
## Examples
6+
7+
### Static Token (`staticToken.ts`)
8+
9+
The simplest authentication method. Use a static access token that doesn't change during the application lifetime.
10+
11+
```bash
12+
DATABRICKS_HOST=<host> DATABRICKS_HTTP_PATH=<path> DATABRICKS_TOKEN=<token> npx ts-node staticToken.ts
13+
```
14+
15+
### External Token (`externalToken.ts`)
16+
17+
Use a callback function to provide tokens dynamically. Useful for integrating with secret managers, vaults, or other token sources. Tokens are automatically cached by the driver.
18+
19+
```bash
20+
DATABRICKS_HOST=<host> DATABRICKS_HTTP_PATH=<path> DATABRICKS_TOKEN=<token> npx ts-node externalToken.ts
21+
```
22+
23+
### Token Federation (`federation.ts`)
24+
25+
Automatically exchange tokens from external identity providers (Azure AD, Google, Okta, etc.) for Databricks-compatible tokens using RFC 8693 token exchange.
26+
27+
```bash
28+
DATABRICKS_HOST=<host> DATABRICKS_HTTP_PATH=<path> AZURE_AD_TOKEN=<token> npx ts-node federation.ts
29+
```
30+
31+
### M2M Federation (`m2mFederation.ts`)
32+
33+
Machine-to-machine token federation with a service principal. Requires a `federationClientId` to identify the service principal to Databricks.
34+
35+
```bash
36+
DATABRICKS_HOST=<host> DATABRICKS_HTTP_PATH=<path> DATABRICKS_CLIENT_ID=<client-id> SERVICE_ACCOUNT_TOKEN=<token> npx ts-node m2mFederation.ts
37+
```
38+
39+
### Custom Token Provider (`customTokenProvider.ts`)
40+
41+
Implement the `ITokenProvider` interface for full control over token management, including custom caching, refresh logic, retry, and error handling.
42+
43+
```bash
44+
DATABRICKS_HOST=<host> DATABRICKS_HTTP_PATH=<path> OAUTH_SERVER_URL=<url> OAUTH_CLIENT_ID=<id> OAUTH_CLIENT_SECRET=<secret> npx ts-node customTokenProvider.ts
45+
```
46+
47+
## Prerequisites
48+
49+
- Node.js 14+
50+
- A Databricks workspace with token federation enabled (for federation examples)
51+
- Valid credentials for your identity provider
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Example: Custom Token Provider Implementation
3+
*
4+
* This example demonstrates how to create a custom token provider by
5+
* implementing the ITokenProvider interface. This gives you full control
6+
* over token management, including custom caching, refresh logic, and
7+
* error handling.
8+
*/
9+
10+
import { DBSQLClient } from '@databricks/sql';
11+
import { ITokenProvider, Token } from '../../lib/connection/auth/tokenProvider';
12+
13+
/**
14+
* Custom token provider that refreshes tokens from a custom OAuth server.
15+
*/
16+
class CustomOAuthTokenProvider implements ITokenProvider {
17+
private readonly oauthServerUrl: string;
18+
19+
private readonly clientId: string;
20+
21+
private readonly clientSecret: string;
22+
23+
constructor(oauthServerUrl: string, clientId: string, clientSecret: string) {
24+
this.oauthServerUrl = oauthServerUrl;
25+
this.clientId = clientId;
26+
this.clientSecret = clientSecret;
27+
}
28+
29+
async getToken(): Promise<Token> {
30+
// eslint-disable-next-line no-console
31+
console.log('Fetching token from custom OAuth server...');
32+
return this.fetchTokenWithRetry(0);
33+
}
34+
35+
/**
36+
* Recursively attempts to fetch a token with exponential backoff.
37+
*/
38+
private async fetchTokenWithRetry(attempt: number): Promise<Token> {
39+
const maxRetries = 3;
40+
41+
try {
42+
return await this.fetchToken();
43+
} catch (error) {
44+
// Don't retry client errors (4xx)
45+
if (error instanceof Error && error.message.includes('OAuth token request failed: 4')) {
46+
throw error;
47+
}
48+
49+
if (attempt >= maxRetries) {
50+
throw error;
51+
}
52+
53+
// Exponential backoff: 1s, 2s, 4s
54+
const delay = 1000 * 2 ** attempt;
55+
await new Promise<void>((resolve) => {
56+
setTimeout(resolve, delay);
57+
});
58+
59+
return this.fetchTokenWithRetry(attempt + 1);
60+
}
61+
}
62+
63+
private async fetchToken(): Promise<Token> {
64+
const response = await fetch(`${this.oauthServerUrl}/oauth/token`, {
65+
method: 'POST',
66+
headers: {
67+
'Content-Type': 'application/x-www-form-urlencoded',
68+
},
69+
body: new URLSearchParams({
70+
grant_type: 'client_credentials',
71+
client_id: this.clientId,
72+
client_secret: this.clientSecret,
73+
scope: 'sql',
74+
}).toString(),
75+
});
76+
77+
if (!response.ok) {
78+
throw new Error(`OAuth token request failed: ${response.status}`);
79+
}
80+
81+
const data = (await response.json()) as {
82+
access_token: string;
83+
token_type?: string;
84+
expires_in?: number;
85+
};
86+
87+
// Calculate expiration
88+
let expiresAt: Date | undefined;
89+
if (typeof data.expires_in === 'number') {
90+
expiresAt = new Date(Date.now() + data.expires_in * 1000);
91+
}
92+
93+
return new Token(data.access_token, {
94+
tokenType: data.token_type ?? 'Bearer',
95+
expiresAt,
96+
});
97+
}
98+
99+
getName(): string {
100+
return 'CustomOAuthTokenProvider';
101+
}
102+
}
103+
104+
/**
105+
* Simple token provider that reads from a file (for development/testing).
106+
*/
107+
// exported for use as an alternative example provider
108+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
109+
class FileTokenProvider implements ITokenProvider {
110+
private readonly filePath: string;
111+
112+
constructor(filePath: string) {
113+
this.filePath = filePath;
114+
}
115+
116+
async getToken(): Promise<Token> {
117+
const fs = await import('fs/promises');
118+
const tokenData = await fs.readFile(this.filePath, 'utf-8');
119+
const parsed = JSON.parse(tokenData);
120+
121+
return Token.fromJWT(parsed.access_token, {
122+
refreshToken: parsed.refresh_token,
123+
});
124+
}
125+
126+
getName(): string {
127+
return 'FileTokenProvider';
128+
}
129+
}
130+
131+
async function main() {
132+
const host = process.env.DATABRICKS_HOST!;
133+
const path = process.env.DATABRICKS_HTTP_PATH!;
134+
135+
const client = new DBSQLClient();
136+
137+
// Option 1: Use a custom OAuth token provider (shown below)
138+
// Option 2: Use a file-based token provider for development:
139+
// const fileProvider = new FileTokenProvider('/path/to/token.json');
140+
const oauthProvider = new CustomOAuthTokenProvider(
141+
process.env.OAUTH_SERVER_URL!,
142+
process.env.OAUTH_CLIENT_ID!,
143+
process.env.OAUTH_CLIENT_SECRET!,
144+
);
145+
146+
await client.connect({
147+
host,
148+
path,
149+
authType: 'token-provider',
150+
tokenProvider: oauthProvider,
151+
// Optionally enable federation if your OAuth server issues non-Databricks tokens
152+
enableTokenFederation: true,
153+
});
154+
155+
console.log('Connected successfully with custom token provider');
156+
157+
// Open a session and run a query
158+
const session = await client.openSession();
159+
const operation = await session.executeStatement('SELECT 1 AS result');
160+
const result = await operation.fetchAll();
161+
162+
console.log('Query result:', result);
163+
164+
await operation.close();
165+
await session.close();
166+
await client.close();
167+
}
168+
169+
main().catch(console.error);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Example: Using an external token provider
3+
*
4+
* This example demonstrates how to use a callback function to provide
5+
* tokens dynamically. This is useful for integrating with secret managers,
6+
* vaults, or other token sources that may refresh tokens.
7+
*/
8+
9+
import { DBSQLClient } from '@databricks/sql';
10+
11+
// Simulate fetching a token from a secret manager or vault
12+
async function fetchTokenFromVault(): Promise<string> {
13+
// In a real application, this would fetch from AWS Secrets Manager,
14+
// Azure Key Vault, HashiCorp Vault, or another secret manager
15+
console.log('Fetching token from vault...');
16+
17+
// Simulated token - replace with actual vault integration
18+
const token = process.env.DATABRICKS_TOKEN!;
19+
return token;
20+
}
21+
22+
async function main() {
23+
const host = process.env.DATABRICKS_HOST!;
24+
const path = process.env.DATABRICKS_HTTP_PATH!;
25+
26+
const client = new DBSQLClient();
27+
28+
// Connect using an external token provider
29+
// The callback will be called each time a new token is needed
30+
// Note: The token is automatically cached, so the callback won't be
31+
// called on every request
32+
await client.connect({
33+
host,
34+
path,
35+
authType: 'external-token',
36+
getToken: fetchTokenFromVault,
37+
});
38+
39+
console.log('Connected successfully with external token provider');
40+
41+
// Open a session and run a query
42+
const session = await client.openSession();
43+
const operation = await session.executeStatement('SELECT current_user() AS user');
44+
const result = await operation.fetchAll();
45+
46+
console.log('Query result:', result);
47+
48+
await operation.close();
49+
await session.close();
50+
await client.close();
51+
}
52+
53+
main().catch(console.error);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Example: Token Federation with an External Identity Provider
3+
*
4+
* This example demonstrates how to use token federation to automatically
5+
* exchange tokens from external identity providers (Azure AD, Google, Okta,
6+
* Auth0, AWS Cognito, GitHub) for Databricks-compatible tokens.
7+
*
8+
* Token federation uses RFC 8693 (OAuth 2.0 Token Exchange) to exchange
9+
* the external JWT token for a Databricks access token.
10+
*/
11+
12+
import { DBSQLClient } from '@databricks/sql';
13+
14+
// Example: Fetch a token from Azure AD
15+
// In a real application, you would use the Azure SDK or similar
16+
async function getAzureADToken(): Promise<string> {
17+
// Example using @azure/identity:
18+
//
19+
// import { DefaultAzureCredential } from '@azure/identity';
20+
// const credential = new DefaultAzureCredential();
21+
// const token = await credential.getToken('https://your-scope/.default');
22+
// return token.token;
23+
24+
// For this example, we use an environment variable
25+
const token = process.env.AZURE_AD_TOKEN!;
26+
console.log('Fetched token from Azure AD');
27+
return token;
28+
}
29+
30+
// Example: Fetch a token from Google
31+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
32+
async function getGoogleToken(): Promise<string> {
33+
// Example using google-auth-library:
34+
//
35+
// import { GoogleAuth } from 'google-auth-library';
36+
// const auth = new GoogleAuth();
37+
// const client = await auth.getClient();
38+
// const token = await client.getAccessToken();
39+
// return token.token;
40+
41+
const token = process.env.GOOGLE_TOKEN!;
42+
console.log('Fetched token from Google');
43+
return token;
44+
}
45+
46+
async function main() {
47+
const host = process.env.DATABRICKS_HOST!;
48+
const path = process.env.DATABRICKS_HTTP_PATH!;
49+
50+
const client = new DBSQLClient();
51+
52+
// Connect using token federation
53+
// The driver will automatically:
54+
// 1. Get the token from the callback
55+
// 2. Check if the token's issuer matches the Databricks host
56+
// 3. If not, exchange the token for a Databricks token via RFC 8693
57+
// 4. Cache the result for subsequent requests
58+
await client.connect({
59+
host,
60+
path,
61+
authType: 'external-token',
62+
getToken: getAzureADToken, // or getGoogleToken, etc.
63+
enableTokenFederation: true,
64+
});
65+
66+
console.log('Connected successfully with token federation');
67+
68+
// Open a session and run a query
69+
const session = await client.openSession();
70+
const operation = await session.executeStatement('SELECT current_user() AS user');
71+
const result = await operation.fetchAll();
72+
73+
console.log('Query result:', result);
74+
75+
await operation.close();
76+
await session.close();
77+
await client.close();
78+
}
79+
80+
main().catch(console.error);

0 commit comments

Comments
 (0)