Skip to content

Commit 1f4e3a0

Browse files
Support client_credentials OIDC auth flow (#116)
1 parent 52290d1 commit 1f4e3a0

File tree

15 files changed

+773
-57
lines changed

15 files changed

+773
-57
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ jobs:
2525
env:
2626
OKTA_DUMMY_CI_PW: ${{ secrets.OKTA_DUMMY_CI_PW }}
2727
WCS_DUMMY_CI_PW: ${{ secrets.WCS_DUMMY_CI_PW }}
28+
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
29+
OKTA_CLIENT_SECRET: ${{ secrets.OKTA_CLIENT_SECRET }}
2830
run: |
2931
npm test
3032
npm run build

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ node_modules/
33
config.yaml
44
lib.js
55
weaviate-data/
6+
.vscode/
7+
.idea/

ci/compose.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ function compose_down_all {
1919
}
2020

2121
function all_weaviate_ports {
22-
echo "8080 8082 8083"
22+
echo "8080 8081 8082 8083 8085"
2323
}

ci/docker-compose-azure-cc.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
---
3+
version: '3.4'
4+
services:
5+
weaviate-auth-azure:
6+
command:
7+
- --host
8+
- 0.0.0.0
9+
- --port
10+
- '8081'
11+
- --scheme
12+
- http
13+
- --write-timeout=600s
14+
image: semitechnologies/weaviate:preview-replace-shardingconfig-replicas-with-replication-factor-29e987d
15+
ports:
16+
- 8081:8081
17+
restart: on-failure:0
18+
environment:
19+
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
20+
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false'
21+
AUTHENTICATION_OIDC_ENABLED: 'true'
22+
AUTHENTICATION_OIDC_CLIENT_ID: '4706508f-30c2-469b-8b12-ad272b3de864'
23+
AUTHENTICATION_OIDC_ISSUER: 'https://login.microsoftonline.com/36c47fb4-f57c-4e1c-8760-d42293932cc2/v2.0'
24+
AUTHENTICATION_OIDC_USERNAME_CLAIM: 'oid'
25+
AUTHENTICATION_OIDC_GROUPS_CLAIM: 'groups'
26+
AUTHORIZATION_ADMINLIST_ENABLED: 'true'
27+
AUTHORIZATION_ADMINLIST_USERS: 'b6bf8e1d-d398-4e5d-8f1b-50fda9146a64'
28+
AUTHENTICATION_OIDC_SCOPES: 'openid,email'
29+
...

ci/docker-compose-okta-cc.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
version: '3.4'
3+
services:
4+
weaviate-auth-okta-cc:
5+
command:
6+
- --host
7+
- 0.0.0.0
8+
- --port
9+
- '8082'
10+
- --scheme
11+
- http
12+
- --write-timeout=600s
13+
image: semitechnologies/weaviate:1.15.4-b7811d4
14+
ports:
15+
- 8082:8082
16+
restart: on-failure:0
17+
environment:
18+
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
19+
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false'
20+
AUTHENTICATION_OIDC_ENABLED: 'true'
21+
AUTHENTICATION_OIDC_CLIENT_ID: '0oa7e9ipdkVZRUcxo5d7'
22+
AUTHENTICATION_OIDC_ISSUER: 'https://dev-32300990.okta.com/oauth2/aus7e9kxbwYQB0eht5d7'
23+
AUTHENTICATION_OIDC_USERNAME_CLAIM: 'cid'
24+
AUTHENTICATION_OIDC_GROUPS_CLAIM: 'groups'
25+
AUTHORIZATION_ADMINLIST_ENABLED: 'true'
26+
AUTHORIZATION_ADMINLIST_USERS: '0oa7e9ipdkVZRUcxo5d7'
27+
AUTHENTICATION_OIDC_SCOPES: 'openid,email'
28+
...

ci/docker-compose-okta.yml renamed to ci/docker-compose-okta-users.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ services:
66
- --host
77
- 0.0.0.0
88
- --port
9-
- '8082'
9+
- '8083'
1010
- --scheme
1111
- http
1212
- --write-timeout=600s
1313
image: semitechnologies/weaviate:1.17.0
1414
ports:
15-
- 8082:8082
15+
- 8083:8083
1616
restart: on-failure:0
1717
environment:
1818
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'

ci/docker-compose-wcs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ services:
66
- --host
77
- 0.0.0.0
88
- --port
9-
- '8083'
9+
- '8085'
1010
- --scheme
1111
- http
1212
- --write-timeout=600s
1313
image: semitechnologies/weaviate:1.17.0
1414
ports:
15-
- 8083:8083
15+
- 8085:8085
1616
restart: on-failure:0
1717
environment:
1818
PERSISTENCE_DATA_PATH: '/var/lib/weaviate'

connection/auth.js

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ export class Authenticator {
22
constructor(http, creds) {
33
this.http = http;
44
this.creds = creds;
5-
this.bearerToken = "";
5+
this.accessToken = "";
66
this.refreshToken = "";
7-
this.expirationEpoch = 0
7+
this.expiresAt = 0
88
this.refreshRunning = false;
99

1010
// If the authentication method is access token,
1111
// our bearer token is already available for use
1212
if (this.creds instanceof AuthAccessTokenCredentials) {
13-
this.bearerToken = this.creds.accessToken;
14-
this.expirationEpoch = calcExpirationEpoch(this.creds.expiresIn);
13+
this.accessToken = this.creds.accessToken;
14+
this.expiresAt = calcExpirationEpoch(this.creds.expiresIn);
1515
this.refreshToken = this.creds.refreshToken;
1616
}
1717
}
@@ -27,14 +27,17 @@ export class Authenticator {
2727
case AuthAccessTokenCredentials:
2828
authenticator = new AccessTokenAuthenticator(this.http, this.creds, config);
2929
break;
30+
case AuthClientCredentials:
31+
authenticator = new ClientCredentialsAuthenticator(this.http, this.creds, config);
32+
break;
3033
default:
3134
throw new Error("unsupported credential type");
3235
}
3336

3437
return authenticator.refresh()
3538
.then(resp => {
36-
this.bearerToken = resp.bearerToken;
37-
this.expirationEpoch = resp.expirationEpoch;
39+
this.accessToken = resp.accessToken;
40+
this.expiresAt = resp.expiresAt;
3841
this.refreshToken = resp.refreshToken;
3942
if (!this.refreshRunning) {
4043
this.runBackgroundTokenRefresh(authenticator);
@@ -47,20 +50,21 @@ export class Authenticator {
4750
return this.http.externalGet(localConfig.href)
4851
.then(openidProviderConfig => {
4952
return {
50-
clientId: localConfig.clientId,
51-
provider: openidProviderConfig
53+
clientId: localConfig.clientId,
54+
provider: openidProviderConfig,
55+
scopes: localConfig.scopes
5256
};
5357
});
5458
};
5559

5660
runBackgroundTokenRefresh = (authenticator) => {
57-
setInterval(async () => {
61+
setInterval(async () => {
5862
// check every 30s if the token will expire in <= 1m,
5963
// if so, refresh
60-
if (this.expirationEpoch - Date.now() <= 60_000) {
64+
if (this.expiresAt - Date.now() <= 60_000) {
6165
var resp = await authenticator.refresh();
62-
this.bearerToken = resp.bearerToken;
63-
this.expirationEpoch = resp.expirationEpoch;
66+
this.accessToken = resp.accessToken;
67+
this.expiresAt = resp.expiresAt;
6468
this.refreshToken = resp.refreshToken;
6569
}
6670
}, 30_000)
@@ -71,6 +75,7 @@ export class AuthUserPasswordCredentials {
7175
constructor(creds) {
7276
this.username = creds.username;
7377
this.password = creds.password;
78+
this.scopes = creds.scopes;
7479
}
7580
}
7681

@@ -79,15 +84,18 @@ class UserPasswordAuthenticator {
7984
this.http = http;
8085
this.creds = creds;
8186
this.openidConfig = config;
87+
if (creds.scopes) {
88+
this.openidConfig.scopes.push(creds.scopes);
89+
}
8290
}
8391

8492
refresh = () => {
8593
this.validateOpenidConfig();
8694
return this.requestAccessToken()
8795
.then(tokenResp => {
8896
return {
89-
bearerToken: tokenResp.access_token,
90-
expirationEpoch: calcExpirationEpoch(tokenResp.expires_in),
97+
accessToken: tokenResp.access_token,
98+
expiresAt: calcExpirationEpoch(tokenResp.expires_in),
9199
refreshToken: tokenResp.refresh_token
92100
};
93101
})
@@ -98,37 +106,38 @@ class UserPasswordAuthenticator {
98106
});
99107
};
100108

109+
validateOpenidConfig = () => {
110+
if (this.openidConfig.provider.grant_types_supported !== undefined &&
111+
!this.openidConfig.provider.grant_types_supported.includes("password")) {
112+
throw new Error("grant_type password not supported");
113+
}
114+
if (this.openidConfig.provider.token_endpoint.includes(
115+
"https://login.microsoftonline.com")) {
116+
throw new Error("microsoft/azure recommends to avoid authentication using " +
117+
"username and password, so this method is not supported by this client");
118+
}
119+
this.openidConfig.scopes.push("offline_access");
120+
};
121+
101122
requestAccessToken = () => {
102123
var url = this.openidConfig.provider.token_endpoint;
103124
var params = new URLSearchParams({
104125
grant_type: "password",
105126
client_id: this.openidConfig.clientId,
106127
username: this.creds.username,
107128
password: this.creds.password,
108-
scope: "openid offline_access"
129+
scope: this.openidConfig.scopes.join(" ")
109130
});
110131
let contentType = "application/x-www-form-urlencoded;charset=UTF-8";
111132
return this.http.externalPost(url, params, contentType);
112133
};
113-
114-
validateOpenidConfig = () => {
115-
if (this.openidConfig.provider.grant_types_supported !== undefined &&
116-
!this.openidConfig.provider.grant_types_supported.includes("password")) {
117-
throw new Error("grant_type password not supported");
118-
}
119-
if (this.openidConfig.provider.token_endpoint.includes(
120-
"https://login.microsoftonline.com")) {
121-
throw new Error("microsoft/azure recommends to avoid authentication using "+
122-
"username and password, so this method is not supported by this client");
123-
}
124-
};
125134
}
126135

127136
export class AuthAccessTokenCredentials {
128137
constructor(creds) {
129138
this.validate(creds);
130139
this.accessToken = creds.accessToken;
131-
this.expirationEpoch = calcExpirationEpoch(creds.expiresIn);
140+
this.expiresAt = calcExpirationEpoch(creds.expiresIn);
132141
this.refreshToken = creds.refreshToken;
133142
}
134143

@@ -153,16 +162,16 @@ class AccessTokenAuthenticator {
153162
if (this.creds.refreshToken === undefined || this.creds.refreshToken == "") {
154163
console.warn("AuthAccessTokenCredentials not provided with refreshToken, cannot refresh");
155164
return Promise.resolve({
156-
bearerToken: this.creds.accessToken,
157-
expirationEpoch: this.creds.expirationEpoch
165+
accessToken: this.creds.accessToken,
166+
expiresAt: this.creds.expiresAt
158167
});
159168
}
160169
this.validateOpenidConfig();
161170
return this.requestAccessToken()
162171
.then(tokenResp => {
163172
return {
164-
bearerToken: tokenResp.access_token,
165-
expirationEpoch: calcExpirationEpoch(tokenResp.expires_in),
173+
accessToken: tokenResp.access_token,
174+
expiresAt: calcExpirationEpoch(tokenResp.expires_in),
166175
refreshToken: tokenResp.refresh_token
167176
};
168177
})
@@ -176,8 +185,8 @@ class AccessTokenAuthenticator {
176185
validateOpenidConfig = () => {
177186
if (this.openidConfig.provider.grant_types_supported === undefined ||
178187
!this.openidConfig.provider.grant_types_supported.includes("refresh_token")) {
179-
throw new Error("grant_type refresh_token not supported");
180-
}
188+
throw new Error("grant_type refresh_token not supported");
189+
}
181190
};
182191

183192
requestAccessToken = () => {
@@ -192,6 +201,66 @@ class AccessTokenAuthenticator {
192201
};
193202
}
194203

204+
export class AuthClientCredentials {
205+
constructor(creds) {
206+
this.clientSecret = creds.clientSecret;
207+
this.scopes = creds.scopes;
208+
}
209+
}
210+
211+
class ClientCredentialsAuthenticator {
212+
constructor(http, creds, config) {
213+
this.http = http;
214+
this.creds = creds;
215+
this.openidConfig = config;
216+
if (creds.scopes) {
217+
this.openidConfig.scopes.push(creds.scopes);
218+
}
219+
}
220+
221+
refresh = () => {
222+
this.validateOpenidConfig();
223+
return this.requestAccessToken()
224+
.then(tokenResp => {
225+
return {
226+
accessToken: tokenResp.access_token,
227+
expiresAt: calcExpirationEpoch(tokenResp.expires_in),
228+
refreshToken: tokenResp.refresh_token
229+
};
230+
})
231+
.catch(err => {
232+
return Promise.reject(
233+
new Error(`failed to refresh access token: ${err}`)
234+
);
235+
});
236+
};
237+
238+
validateOpenidConfig = () => {
239+
this.openidConfig.scopes = this.openidConfig.scopes
240+
.filter(scope => scope != "openid" && scope != "email");
241+
if (this.openidConfig.scopes.length > 0) {
242+
return;
243+
}
244+
if (this.openidConfig.provider.token_endpoint
245+
.includes("https://login.microsoftonline.com")) {
246+
this.openidConfig.scopes.push(this.openidConfig.clientId + "/.default");
247+
}
248+
};
249+
250+
requestAccessToken = () => {
251+
var url = this.openidConfig.provider.token_endpoint;
252+
var params = new URLSearchParams({
253+
grant_type: "client_credentials",
254+
client_id: this.openidConfig.clientId,
255+
client_secret: this.creds.clientSecret,
256+
scope: this.openidConfig.scopes.join(" ")
257+
});
258+
259+
let contentType = "application/x-www-form-urlencoded;charset=UTF-8";
260+
return this.http.externalPost(url, params, contentType);
261+
};
262+
}
263+
195264
function calcExpirationEpoch(expiresIn) {
196265
return Date.now() + ((expiresIn - 2) * 1000) // -2 for some lag
197266
}

connection/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ export default class Connection {
9191
return "";
9292
}
9393

94-
if (Date.now() >= this.auth.expirationEpoch) {
94+
if (Date.now() >= this.auth.expiresAt) {
9595
await this.auth.refresh(localConfig);
9696
}
97-
return this.auth.bearerToken;
97+
return this.auth.accessToken;
9898
};
9999
}

0 commit comments

Comments
 (0)