Skip to content

Commit e27aedb

Browse files
Add auth features (#47) (#94)
* Add auth features * Update schemaregistry/rest-service.ts * Minor changes * Minor changes (missed a few) --------- Co-authored-by: Robert Yokota <[email protected]>
1 parent f97166f commit e27aedb

File tree

3 files changed

+118
-27
lines changed

3 files changed

+118
-27
lines changed

schemaregistry/rest-service.ts

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, CreateAxiosDefaults } from 'axios';
22
import { OAuthClient } from './oauth/oauth-client';
33
import { RestError } from './rest-error';
4-
54
/*
65
* Confluent-Schema-Registry-TypeScript - Node.js wrapper for Confluent Schema Registry
76
*
@@ -11,14 +10,27 @@ import { RestError } from './rest-error';
1110
* of the MIT license. See the LICENSE.txt file for details.
1211
*/
1312

13+
export interface BasicAuthCredentials {
14+
credentialsSource: 'USER_INFO' | 'URL' | 'SASL_INHERIT',
15+
userInfo?: string,
16+
saslInfo?: SaslInfo
17+
}
18+
19+
export interface SaslInfo {
20+
mechanism?: string,
21+
username: string,
22+
password: string
23+
}
24+
1425
export interface BearerAuthCredentials {
15-
clientId: string,
16-
clientSecret: string,
17-
tokenHost: string,
18-
tokenPath: string,
19-
schemaRegistryLogicalCluster: string,
20-
identityPool: string,
21-
scope: string
26+
credentialsSource: 'STATIC_TOKEN' | 'OAUTHBEARER',
27+
token?: string,
28+
issuerEndpointUrl?: string,
29+
clientId?: string,
30+
clientSecret?: string,
31+
scope?: string,
32+
logicalCluster?: string,
33+
identityPoolId?: string,
2234
}
2335

2436
//TODO: Consider retry policy, may need additional libraries on top of Axios
@@ -28,33 +40,109 @@ export interface ClientConfig {
2840
cacheLatestTtlSecs?: number,
2941
isForward?: boolean,
3042
createAxiosDefaults?: CreateAxiosDefaults,
43+
basicAuthCredentials?: BasicAuthCredentials,
3144
bearerAuthCredentials?: BearerAuthCredentials,
3245
}
3346

47+
const toBase64 = (str: string): string => Buffer.from(str).toString('base64');
48+
3449
export class RestService {
3550
private client: AxiosInstance;
3651
private baseURLs: string[];
37-
private OAuthClient?: OAuthClient;
38-
private bearerAuth: boolean = false;
52+
private oauthClient?: OAuthClient;
53+
private oauthBearer: boolean = false;
3954

4055
constructor(baseURLs: string[], isForward?: boolean, axiosDefaults?: CreateAxiosDefaults,
41-
bearerAuthCredentials?: BearerAuthCredentials) {
56+
basicAuthCredentials?: BasicAuthCredentials, bearerAuthCredentials?: BearerAuthCredentials) {
4257
this.client = axios.create(axiosDefaults);
4358
this.baseURLs = baseURLs;
4459

4560
if (isForward) {
4661
this.client.defaults.headers.common['X-Forward'] = 'true'
4762
}
4863

64+
this.handleBasicAuth(basicAuthCredentials);
65+
this.handleBearerAuth(bearerAuthCredentials);
66+
67+
if (!basicAuthCredentials && !bearerAuthCredentials) {
68+
throw new Error('No auth credentials provided');
69+
}
70+
}
71+
72+
handleBasicAuth(basicAuthCredentials?: BasicAuthCredentials): void {
73+
if (basicAuthCredentials) {
74+
switch (basicAuthCredentials.credentialsSource) {
75+
case 'USER_INFO':
76+
if (!basicAuthCredentials.userInfo) {
77+
throw new Error('User info not provided');
78+
}
79+
this.setAuth(toBase64(basicAuthCredentials.userInfo!));
80+
break;
81+
case 'SASL_INHERIT':
82+
if (!basicAuthCredentials.saslInfo) {
83+
throw new Error('Sasl info not provided');
84+
}
85+
if (basicAuthCredentials.saslInfo.mechanism?.toUpperCase() === 'GSSAPI') {
86+
throw new Error('SASL_INHERIT support PLAIN and SCRAM SASL mechanisms only');
87+
}
88+
this.setAuth(toBase64(`${basicAuthCredentials.saslInfo.username}:${basicAuthCredentials.saslInfo.password}`));
89+
break;
90+
case 'URL':
91+
if (!basicAuthCredentials.userInfo) {
92+
throw new Error('User info not provided');
93+
}
94+
const basicAuthUrl = new URL(basicAuthCredentials.userInfo);
95+
this.setAuth(toBase64(`${basicAuthUrl.username}:${basicAuthUrl.password}`));
96+
break;
97+
default:
98+
throw new Error('Invalid basic auth credentials source');
99+
}
100+
}
101+
}
102+
103+
handleBearerAuth(bearerAuthCredentials?: BearerAuthCredentials): void {
49104
if (bearerAuthCredentials) {
50-
this.bearerAuth = true;
51105
delete this.client.defaults.auth;
106+
107+
const headers = ['logicalCluster', 'identityPoolId'];
108+
const missingHeaders = headers.find(header => bearerAuthCredentials[header as keyof typeof bearerAuthCredentials]);
109+
110+
if (missingHeaders) {
111+
throw new Error(`Bearer auth header '${missingHeaders}' not provided`);
112+
}
113+
52114
this.setHeaders({
53-
'Confluent-Identity-Pool-Id': bearerAuthCredentials.identityPool,
54-
'target-sr-cluster': bearerAuthCredentials.schemaRegistryLogicalCluster
115+
'Confluent-Identity-Pool-Id': bearerAuthCredentials.identityPoolId!,
116+
'target-sr-cluster': bearerAuthCredentials.logicalCluster!
55117
});
56-
this.OAuthClient = new OAuthClient(bearerAuthCredentials.clientId, bearerAuthCredentials.clientSecret,
57-
bearerAuthCredentials.tokenHost, bearerAuthCredentials.tokenPath, bearerAuthCredentials.scope);
118+
119+
switch (bearerAuthCredentials.credentialsSource) {
120+
case 'STATIC_TOKEN':
121+
if (!bearerAuthCredentials.token) {
122+
throw new Error('Bearer token not provided');
123+
}
124+
this.setAuth(undefined, bearerAuthCredentials.token);
125+
break;
126+
case 'OAUTHBEARER':
127+
this.oauthBearer = true;
128+
const requiredFields = [
129+
'clientId',
130+
'clientSecret',
131+
'issuerEndpointUrl',
132+
'scope'
133+
];
134+
const missingField = requiredFields.find(field => bearerAuthCredentials[field as keyof typeof bearerAuthCredentials]);
135+
136+
if (missingField) {
137+
throw new Error(`OAuth credential '${missingField}' not provided`);
138+
}
139+
const issuerEndPointUrl = new URL(bearerAuthCredentials.issuerEndpointUrl!);
140+
this.oauthClient = new OAuthClient(bearerAuthCredentials.clientId!, bearerAuthCredentials.clientSecret!,
141+
issuerEndPointUrl.host, issuerEndPointUrl.pathname, bearerAuthCredentials.scope!);
142+
break;
143+
default:
144+
throw new Error('Invalid bearer auth credentials source');
145+
}
58146
}
59147
}
60148

@@ -65,8 +153,8 @@ export class RestService {
65153
config?: AxiosRequestConfig,
66154
): Promise<AxiosResponse<T>> {
67155

68-
if (this.bearerAuth) {
69-
await this.setBearerToken();
156+
if (this.oauthBearer) {
157+
await this.setOAuthBearerToken();
70158
}
71159

72160
for (let i = 0; i < this.baseURLs.length; i++) {
@@ -111,12 +199,12 @@ export class RestService {
111199
}
112200
}
113201

114-
async setBearerToken(): Promise<void> {
115-
if (!this.OAuthClient) {
202+
async setOAuthBearerToken(): Promise<void> {
203+
if (!this.oauthClient) {
116204
throw new Error('OAuthClient not initialized');
117205
}
118206

119-
const bearerToken: string = await this.OAuthClient.getAccessToken();
207+
const bearerToken: string = await this.oauthClient.getAccessToken();
120208
this.setAuth(undefined, bearerToken);
121209
}
122210

schemaregistry/schemaregistry-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class SchemaRegistryClient implements Client {
166166
};
167167

168168
this.restService = new RestService(config.baseURLs, config.isForward, config.createAxiosDefaults,
169-
config.bearerAuthCredentials);
169+
config.basicAuthCredentials, config.bearerAuthCredentials);
170170

171171
this.schemaToIdCache = new LRUCache(cacheOptions);
172172
this.idToSchemaInfoCache = new LRUCache(cacheOptions);

test/schemaregistry/test-constants.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CreateAxiosDefaults } from 'axios';
2-
import { ClientConfig } from '../../schemaregistry/rest-service';
2+
import { ClientConfig, BasicAuthCredentials } from '../../schemaregistry/rest-service';
33

44
const baseUrls = ['http://localhost:8081'];
55

@@ -9,19 +9,21 @@ const createAxiosDefaults: CreateAxiosDefaults = {
99
headers: {
1010
'Content-Type': 'application/vnd.schemaregistry.v1+json',
1111
},
12-
auth: {
13-
username: 'RBACAllowedUser-lsrc1',
14-
password: 'nohash',
15-
},
1612
timeout: 10000
1713
};
1814

15+
const basicAuthCredentials: BasicAuthCredentials = {
16+
credentialsSource: 'USER_INFO',
17+
userInfo: 'RBACAllowedUser-lsrc1:nohash',
18+
};
19+
1920
const clientConfig: ClientConfig = {
2021
baseURLs: baseUrls,
2122
createAxiosDefaults: createAxiosDefaults,
2223
isForward: false,
2324
cacheCapacity: 512,
2425
cacheLatestTtlSecs: 60,
26+
basicAuthCredentials: basicAuthCredentials,
2527
};
2628

2729
const mockClientConfig: ClientConfig = {
@@ -30,6 +32,7 @@ const mockClientConfig: ClientConfig = {
3032
isForward: false,
3133
cacheCapacity: 512,
3234
cacheLatestTtlSecs: 60,
35+
basicAuthCredentials: basicAuthCredentials
3336
};
3437

3538
export { clientConfig, mockClientConfig };

0 commit comments

Comments
 (0)