Skip to content

Commit 565b8bc

Browse files
Explicit support for API key authentication (#14)
1 parent 13660f8 commit 565b8bc

File tree

8 files changed

+205
-95
lines changed

8 files changed

+205
-95
lines changed

ci/docker-compose-wcs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ services:
2525
AUTHORIZATION_ADMINLIST_ENABLED: 'true'
2626
AUTHORIZATION_ADMINLIST_USERS: '[email protected]'
2727
AUTHENTICATION_OIDC_SCOPES: 'openid,email'
28+
AUTHENTICATION_APIKEY_ENABLED: 'true'
29+
AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'my-secret-key'
30+
AUTHENTICATION_APIKEY_USERS: '[email protected]'
2831
...

examples/javascript/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ console.log(
3131
)
3232
);
3333

34+
console.log(JSON.stringify(new weaviate.ApiKey('abcd1234')));
35+
3436
console.log(weaviate.backup.Backend.GCS);
3537
console.log(weaviate.batch.DeleteOutput.MINIMAL);
3638
console.log(weaviate.cluster.NodeStatus.HEALTHY);

examples/typescript/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ console.log(
3131
)
3232
);
3333

34+
console.log(JSON.stringify(new weaviate.ApiKey('abcd1234')));
35+
3436
console.log(weaviate.backup.Backend.GCS);
3537
console.log(weaviate.batch.DeleteOutput.MINIMAL);
3638
console.log(weaviate.cluster.NodeStatus.HEALTHY);

src/connection/auth.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ interface AuthenticatorResult {
66
refreshToken: string;
77
}
88

9-
interface OIDCAuthFlow {
9+
export interface OidcAuthFlow {
1010
refresh: () => Promise<AuthenticatorResult>;
1111
}
1212

13-
export class Authenticator {
13+
export class OidcAuthenticator {
1414
private readonly http: HttpClient;
1515
private readonly creds: any;
1616
private accessToken: string;
@@ -38,7 +38,7 @@ export class Authenticator {
3838
refresh = async (localConfig: any) => {
3939
const config = await this.getOpenidConfig(localConfig);
4040

41-
let authenticator: OIDCAuthFlow;
41+
let authenticator: OidcAuthFlow;
4242
switch (this.creds.constructor) {
4343
case AuthUserPasswordCredentials:
4444
authenticator = new UserPasswordAuthenticator(
@@ -105,6 +105,18 @@ export class Authenticator {
105105
refreshTokenProvided = () => {
106106
return this.refreshToken && this.refreshToken != '';
107107
};
108+
109+
getAccessToken = () => {
110+
return this.accessToken;
111+
};
112+
113+
getExpiresAt = () => {
114+
return this.expiresAt;
115+
};
116+
117+
resetExpiresAt() {
118+
this.expiresAt = 0;
119+
}
108120
}
109121

110122
export interface UserPasswordCredentialsInput {
@@ -130,7 +142,7 @@ interface RequestAccessTokenResponse {
130142
refresh_token: string;
131143
}
132144

133-
class UserPasswordAuthenticator implements OIDCAuthFlow {
145+
class UserPasswordAuthenticator implements OidcAuthFlow {
134146
private creds: any;
135147
private http: any;
136148
private openidConfig: any;
@@ -222,7 +234,7 @@ export class AuthAccessTokenCredentials {
222234
};
223235
}
224236

225-
class AccessTokenAuthenticator implements OIDCAuthFlow {
237+
class AccessTokenAuthenticator implements OidcAuthFlow {
226238
private creds: any;
227239
private http: any;
228240
private openidConfig: any;
@@ -299,7 +311,7 @@ export class AuthClientCredentials {
299311
}
300312
}
301313

302-
class ClientCredentialsAuthenticator implements OIDCAuthFlow {
314+
class ClientCredentialsAuthenticator implements OidcAuthFlow {
303315
private creds: any;
304316
private http: any;
305317
private openidConfig: any;
@@ -357,6 +369,14 @@ class ClientCredentialsAuthenticator implements OIDCAuthFlow {
357369
};
358370
}
359371

372+
export class ApiKey {
373+
public readonly apiKey: string;
374+
375+
constructor(apiKey: string) {
376+
this.apiKey = apiKey;
377+
}
378+
}
379+
360380
function calcExpirationEpoch(expiresIn: number): number {
361381
return Date.now() + (expiresIn - 2) * 1000; // -2 for some lag
362382
}

src/connection/index.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
1-
import { Authenticator } from './auth';
1+
import { ApiKey, OidcAuthenticator } from './auth';
22
import OpenidConfigurationGetter from '../misc/openidConfigurationGetter';
33

44
import httpClient, { HttpClient } from './httpClient';
55
import gqlClient, { GraphQLClient } from './gqlClient';
66
import { ConnectionParams } from '../index';
77

88
export default class Connection {
9-
public readonly auth: any;
10-
private readonly authEnabled: boolean;
9+
private apiKey?: string;
10+
private oidcAuth?: OidcAuthenticator;
11+
private authEnabled: boolean;
1112
private gql: GraphQLClient;
1213
public readonly http: HttpClient;
1314

1415
constructor(params: ConnectionParams) {
1516
this.http = httpClient(params);
1617
this.gql = gqlClient(params);
18+
this.authEnabled = this.parseAuthParams(params);
19+
}
1720

18-
this.authEnabled = params.authClientSecret !== undefined;
19-
if (this.authEnabled) {
20-
this.auth = new Authenticator(this.http, params.authClientSecret);
21+
private parseAuthParams(params: ConnectionParams): boolean {
22+
if (params.authClientSecret && params.apiKey) {
23+
throw new Error(
24+
'must provide one of authClientSecret (OIDC) or apiKey, cannot provide both'
25+
);
26+
}
27+
if (params.authClientSecret) {
28+
this.oidcAuth = new OidcAuthenticator(this.http, params.authClientSecret);
29+
return true;
30+
}
31+
if (params.apiKey) {
32+
this.apiKey = params.apiKey?.apiKey;
33+
return true;
2134
}
35+
return false;
2236
}
2337

2438
post = (path: string, payload: any, expectReturnContent = true) => {
@@ -84,6 +98,14 @@ export default class Connection {
8498
};
8599

86100
login = async () => {
101+
if (this.apiKey) {
102+
return this.apiKey;
103+
}
104+
105+
if (!this.oidcAuth) {
106+
return '';
107+
}
108+
87109
const localConfig = await new OpenidConfigurationGetter(this.http).do();
88110

89111
if (localConfig === undefined) {
@@ -93,9 +115,9 @@ export default class Connection {
93115
return '';
94116
}
95117

96-
if (Date.now() >= this.auth.expiresAt) {
97-
await this.auth.refresh(localConfig);
118+
if (Date.now() >= this.oidcAuth.getExpiresAt()) {
119+
await this.oidcAuth.refresh(localConfig);
98120
}
99-
return this.auth.accessToken;
121+
return this.oidcAuth.getAccessToken();
100122
};
101123
}

src/connection/journey.test.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ApiKey,
23
AuthAccessTokenCredentials,
34
AuthClientCredentials,
45
AuthUserPasswordCredentials,
@@ -151,6 +152,24 @@ describe('connection', () => {
151152
});
152153
});
153154

155+
it('makes a logged-in request with API key', () => {
156+
const client = weaviate.client({
157+
scheme: 'http',
158+
host: 'localhost:8085',
159+
apiKey: new ApiKey('my-secret-key'),
160+
});
161+
162+
return client.misc
163+
.metaGetter()
164+
.do()
165+
.then((res: any) => {
166+
expect(res.version).toBeDefined();
167+
})
168+
.catch((e: any) => {
169+
throw new Error('it should not have errord: ' + e);
170+
});
171+
});
172+
154173
it('makes a logged-in request with access token', async () => {
155174
if (
156175
process.env.WCS_DUMMY_CI_PW == undefined ||
@@ -172,11 +191,12 @@ describe('connection', () => {
172191
// use it to test AuthAccessTokenCredentials
173192
await dummy.login();
174193

194+
const accessToken = (dummy as any).oidcAuth?.accessToken || '';
175195
const client = weaviate.client({
176196
scheme: 'http',
177197
host: 'localhost:8085',
178198
authClientSecret: new AuthAccessTokenCredentials({
179-
accessToken: dummy.auth.accessToken,
199+
accessToken: accessToken,
180200
expiresIn: 900,
181201
}),
182202
});
@@ -213,17 +233,18 @@ describe('connection', () => {
213233
// use it to test AuthAccessTokenCredentials
214234
await dummy.login();
215235

236+
const accessToken = (dummy as any).oidcAuth?.accessToken || '';
216237
const conn = new Connection({
217238
scheme: 'http',
218239
host: 'localhost:8085',
219240
authClientSecret: new AuthAccessTokenCredentials({
220-
accessToken: dummy.auth.accessToken,
241+
accessToken: accessToken,
221242
expiresIn: 1,
222-
refreshToken: dummy.auth.refreshToken,
243+
refreshToken: (dummy as any).oidcAuth?.refreshToken,
223244
}),
224245
});
225246
// force the use of refreshToken
226-
conn.auth.expiresAt = 0;
247+
(conn as any).oidcAuth?.resetExpiresAt();
227248

228249
return conn
229250
.login()
@@ -293,7 +314,7 @@ describe('connection', () => {
293314
}),
294315
});
295316
// force the use of refreshToken
296-
conn.auth.expiresAt = 0;
317+
(conn as any).oidcAuth?.resetExpiresAt();
297318

298319
await conn
299320
.login()
@@ -309,4 +330,21 @@ describe('connection', () => {
309330
'AuthAccessTokenCredentials not provided with refreshToken, cannot refresh'
310331
);
311332
});
333+
334+
it('fails to create client with both OIDC creds and API key set', () => {
335+
expect(() => {
336+
// eslint-disable-next-line no-new
337+
new Connection({
338+
scheme: 'http',
339+
host: 'localhost:8085',
340+
authClientSecret: new AuthAccessTokenCredentials({
341+
accessToken: 'abcd1234',
342+
expiresIn: 1,
343+
}),
344+
apiKey: new ApiKey('some-key'),
345+
});
346+
}).toThrow(
347+
'must provide one of authClientSecret (OIDC) or apiKey, cannot provide both'
348+
);
349+
});
312350
});

0 commit comments

Comments
 (0)