Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ci/docker-compose-wcs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ services:
AUTHORIZATION_ADMINLIST_ENABLED: 'true'
AUTHORIZATION_ADMINLIST_USERS: 'ms_2d0e007e7136de11d5f29fce7a53dae219a51458@existiert.net'
AUTHENTICATION_OIDC_SCOPES: 'openid,email'
AUTHENTICATION_APIKEY_ENABLED: 'true'
AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'my-secret-key'
AUTHENTICATION_APIKEY_USERS: 'ms_2d0e007e7136de11d5f29fce7a53dae219a51458@existiert.net'
...
2 changes: 2 additions & 0 deletions examples/javascript/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ console.log(
)
);

console.log(JSON.stringify(new weaviate.ApiKey('abcd1234')));

console.log(weaviate.backup.Backend.GCS);
console.log(weaviate.batch.DeleteOutput.MINIMAL);
console.log(weaviate.cluster.NodeStatus.HEALTHY);
Expand Down
2 changes: 2 additions & 0 deletions examples/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ console.log(
)
);

console.log(JSON.stringify(new weaviate.ApiKey('abcd1234')));

console.log(weaviate.backup.Backend.GCS);
console.log(weaviate.batch.DeleteOutput.MINIMAL);
console.log(weaviate.cluster.NodeStatus.HEALTHY);
Expand Down
32 changes: 26 additions & 6 deletions src/connection/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ interface AuthenticatorResult {
refreshToken: string;
}

interface OIDCAuthFlow {
export interface OidcAuthFlow {
refresh: () => Promise<AuthenticatorResult>;
}

export class Authenticator {
export class OidcAuthenticator {
private readonly http: HttpClient;
private readonly creds: any;
private accessToken: string;
Expand Down Expand Up @@ -38,7 +38,7 @@ export class Authenticator {
refresh = async (localConfig: any) => {
const config = await this.getOpenidConfig(localConfig);

let authenticator: OIDCAuthFlow;
let authenticator: OidcAuthFlow;
switch (this.creds.constructor) {
case AuthUserPasswordCredentials:
authenticator = new UserPasswordAuthenticator(
Expand Down Expand Up @@ -105,6 +105,18 @@ export class Authenticator {
refreshTokenProvided = () => {
return this.refreshToken && this.refreshToken != '';
};

getAccessToken = () => {
return this.accessToken;
};

getExpiresAt = () => {
return this.expiresAt;
};

resetExpiresAt() {
this.expiresAt = 0;
}
}

export interface UserPasswordCredentialsInput {
Expand All @@ -130,7 +142,7 @@ interface RequestAccessTokenResponse {
refresh_token: string;
}

class UserPasswordAuthenticator implements OIDCAuthFlow {
class UserPasswordAuthenticator implements OidcAuthFlow {
private creds: any;
private http: any;
private openidConfig: any;
Expand Down Expand Up @@ -222,7 +234,7 @@ export class AuthAccessTokenCredentials {
};
}

class AccessTokenAuthenticator implements OIDCAuthFlow {
class AccessTokenAuthenticator implements OidcAuthFlow {
private creds: any;
private http: any;
private openidConfig: any;
Expand Down Expand Up @@ -299,7 +311,7 @@ export class AuthClientCredentials {
}
}

class ClientCredentialsAuthenticator implements OIDCAuthFlow {
class ClientCredentialsAuthenticator implements OidcAuthFlow {
private creds: any;
private http: any;
private openidConfig: any;
Expand Down Expand Up @@ -357,6 +369,14 @@ class ClientCredentialsAuthenticator implements OIDCAuthFlow {
};
}

export class ApiKey {
public readonly apiKey: string;

constructor(apiKey: string) {
this.apiKey = apiKey;
}
}

function calcExpirationEpoch(expiresIn: number): number {
return Date.now() + (expiresIn - 2) * 1000; // -2 for some lag
}
40 changes: 31 additions & 9 deletions src/connection/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
import { Authenticator } from './auth';
import { ApiKey, OidcAuthenticator } from './auth';
import OpenidConfigurationGetter from '../misc/openidConfigurationGetter';

import httpClient, { HttpClient } from './httpClient';
import gqlClient, { GraphQLClient } from './gqlClient';
import { ConnectionParams } from '../index';

export default class Connection {
public readonly auth: any;
private readonly authEnabled: boolean;
private apiKey?: string;
private oidcAuth?: OidcAuthenticator;
private authEnabled: boolean;
private gql: GraphQLClient;
public readonly http: HttpClient;

constructor(params: ConnectionParams) {
this.http = httpClient(params);
this.gql = gqlClient(params);
this.authEnabled = this.parseAuthParams(params);
}

this.authEnabled = params.authClientSecret !== undefined;
if (this.authEnabled) {
this.auth = new Authenticator(this.http, params.authClientSecret);
private parseAuthParams(params: ConnectionParams): boolean {
if (params.authClientSecret && params.apiKey) {
throw new Error(
'must provide one of authClientSecret (OIDC) or apiKey, cannot provide both'
);
}
if (params.authClientSecret) {
this.oidcAuth = new OidcAuthenticator(this.http, params.authClientSecret);
return true;
}
if (params.apiKey) {
this.apiKey = params.apiKey?.apiKey;
return true;
}
return false;
}

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

login = async () => {
if (this.apiKey) {
return this.apiKey;
}

if (!this.oidcAuth) {
return '';
}

const localConfig = await new OpenidConfigurationGetter(this.http).do();

if (localConfig === undefined) {
Expand All @@ -93,9 +115,9 @@ export default class Connection {
return '';
}

if (Date.now() >= this.auth.expiresAt) {
await this.auth.refresh(localConfig);
if (Date.now() >= this.oidcAuth.getExpiresAt()) {
await this.oidcAuth.refresh(localConfig);
}
return this.auth.accessToken;
return this.oidcAuth.getAccessToken();
};
}
48 changes: 43 additions & 5 deletions src/connection/journey.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ApiKey,
AuthAccessTokenCredentials,
AuthClientCredentials,
AuthUserPasswordCredentials,
Expand Down Expand Up @@ -151,6 +152,24 @@ describe('connection', () => {
});
});

it('makes a logged-in request with API key', () => {
const client = weaviate.client({
scheme: 'http',
host: 'localhost:8085',
apiKey: new ApiKey('my-secret-key'),
});

return client.misc
.metaGetter()
.do()
.then((res: any) => {
expect(res.version).toBeDefined();
})
.catch((e: any) => {
throw new Error('it should not have errord: ' + e);
});
});

it('makes a logged-in request with access token', async () => {
if (
process.env.WCS_DUMMY_CI_PW == undefined ||
Expand All @@ -172,11 +191,12 @@ describe('connection', () => {
// use it to test AuthAccessTokenCredentials
await dummy.login();

const accessToken = (dummy as any).oidcAuth?.accessToken || '';
const client = weaviate.client({
scheme: 'http',
host: 'localhost:8085',
authClientSecret: new AuthAccessTokenCredentials({
accessToken: dummy.auth.accessToken,
accessToken: accessToken,
expiresIn: 900,
}),
});
Expand Down Expand Up @@ -213,17 +233,18 @@ describe('connection', () => {
// use it to test AuthAccessTokenCredentials
await dummy.login();

const accessToken = (dummy as any).oidcAuth?.accessToken || '';
const conn = new Connection({
scheme: 'http',
host: 'localhost:8085',
authClientSecret: new AuthAccessTokenCredentials({
accessToken: dummy.auth.accessToken,
accessToken: accessToken,
expiresIn: 1,
refreshToken: dummy.auth.refreshToken,
refreshToken: (dummy as any).oidcAuth?.refreshToken,
}),
});
// force the use of refreshToken
conn.auth.expiresAt = 0;
(conn as any).oidcAuth?.resetExpiresAt();

return conn
.login()
Expand Down Expand Up @@ -293,7 +314,7 @@ describe('connection', () => {
}),
});
// force the use of refreshToken
conn.auth.expiresAt = 0;
(conn as any).oidcAuth?.resetExpiresAt();

await conn
.login()
Expand All @@ -309,4 +330,21 @@ describe('connection', () => {
'AuthAccessTokenCredentials not provided with refreshToken, cannot refresh'
);
});

it('fails to create client with both OIDC creds and API key set', () => {
expect(() => {
// eslint-disable-next-line no-new
new Connection({
scheme: 'http',
host: 'localhost:8085',
authClientSecret: new AuthAccessTokenCredentials({
accessToken: 'abcd1234',
expiresIn: 1,
}),
apiKey: new ApiKey('some-key'),
});
}).toThrow(
'must provide one of authClientSecret (OIDC) or apiKey, cannot provide both'
);
});
});
Loading