@credibledata/cred-cli 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +2 -1
- package/README.md +0 -2
- package/bun.lock +182 -283
- package/dist/client/api.js +796 -111
- package/dist/client/api.js.map +1 -1
- package/dist/clientconfiguration.js +55 -13
- package/dist/clientconfiguration.js.map +1 -1
- package/dist/clientconfiguration.spec.js +305 -16
- package/dist/clientconfiguration.spec.js.map +1 -1
- package/dist/commands/add.js +6 -3
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/connection/createconnections.js +18 -3
- package/dist/commands/connection/createconnections.js.map +1 -1
- package/dist/commands/connection/createconnections.spec.js +219 -0
- package/dist/commands/connection/createconnections.spec.js.map +1 -0
- package/dist/commands/connection/deleteconnection.spec.js +130 -0
- package/dist/commands/connection/deleteconnection.spec.js.map +1 -0
- package/dist/commands/connection/listconnections.js +9 -3
- package/dist/commands/connection/listconnections.js.map +1 -1
- package/dist/commands/connection/listconnections.spec.js +117 -0
- package/dist/commands/connection/listconnections.spec.js.map +1 -0
- package/dist/commands/group/addmember.js +2 -0
- package/dist/commands/group/addmember.js.map +1 -1
- package/dist/commands/group/addmember.spec.js +148 -0
- package/dist/commands/group/addmember.spec.js.map +1 -0
- package/dist/commands/group/creategroup.spec.js +122 -0
- package/dist/commands/group/creategroup.spec.js.map +1 -0
- package/dist/commands/group/deletegroup.spec.js +104 -0
- package/dist/commands/group/deletegroup.spec.js.map +1 -0
- package/dist/commands/group/getgroup.spec.js +111 -0
- package/dist/commands/group/getgroup.spec.js.map +1 -0
- package/dist/commands/group/listgroups.spec.js +118 -0
- package/dist/commands/group/listgroups.spec.js.map +1 -0
- package/dist/commands/group/removemember.js +1 -0
- package/dist/commands/group/removemember.js.map +1 -1
- package/dist/commands/group/removemember.spec.js +138 -0
- package/dist/commands/group/removemember.spec.js.map +1 -0
- package/dist/commands/groupaccesstoken/createToken.js +6 -2
- package/dist/commands/groupaccesstoken/createToken.js.map +1 -1
- package/dist/commands/groupaccesstoken/createToken.spec.js +127 -0
- package/dist/commands/groupaccesstoken/createToken.spec.js.map +1 -0
- package/dist/commands/groupaccesstoken/deleteToken.js.map +1 -1
- package/dist/commands/groupaccesstoken/deleteToken.spec.js +103 -0
- package/dist/commands/groupaccesstoken/deleteToken.spec.js.map +1 -0
- package/dist/commands/groupaccesstoken/groupContainer.js.map +1 -1
- package/dist/commands/groupaccesstoken/listTokens.js.map +1 -1
- package/dist/commands/groupaccesstoken/listTokens.spec.js +115 -0
- package/dist/commands/groupaccesstoken/listTokens.spec.js.map +1 -0
- package/dist/commands/list.js +10 -3
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/misc/login.js +146 -22
- package/dist/commands/misc/login.js.map +1 -1
- package/dist/commands/misc/login.spec.js +111 -6
- package/dist/commands/misc/login.spec.js.map +1 -1
- package/dist/commands/misc/logout.spec.js +30 -0
- package/dist/commands/misc/logout.spec.js.map +1 -0
- package/dist/commands/misc/loopbackOAuth.js +110 -0
- package/dist/commands/misc/loopbackOAuth.js.map +1 -0
- package/dist/commands/misc/set-access-token.js.map +1 -1
- package/dist/commands/misc/set-access-token.spec.js +65 -0
- package/dist/commands/misc/set-access-token.spec.js.map +1 -0
- package/dist/commands/package/deletepackage.spec.js +136 -0
- package/dist/commands/package/deletepackage.spec.js.map +1 -0
- package/dist/commands/package/listpackages.js +10 -5
- package/dist/commands/package/listpackages.js.map +1 -1
- package/dist/commands/package/listpackages.spec.js +129 -0
- package/dist/commands/package/listpackages.spec.js.map +1 -0
- package/dist/commands/package/publish.js +23 -0
- package/dist/commands/package/publish.js.map +1 -1
- package/dist/commands/package/publish.spec.js +511 -0
- package/dist/commands/package/publish.spec.js.map +1 -0
- package/dist/commands/package/updatepackage.spec.js +50 -0
- package/dist/commands/package/updatepackage.spec.js.map +1 -1
- package/dist/commands/project/createproject.js +11 -1
- package/dist/commands/project/createproject.js.map +1 -1
- package/dist/commands/project/createproject.spec.js +154 -0
- package/dist/commands/project/createproject.spec.js.map +1 -0
- package/dist/commands/project/deleteproject.spec.js +106 -0
- package/dist/commands/project/deleteproject.spec.js.map +1 -0
- package/dist/commands/project/getproject.spec.js +99 -0
- package/dist/commands/project/getproject.spec.js.map +1 -0
- package/dist/commands/project/listprojects.js +13 -7
- package/dist/commands/project/listprojects.js.map +1 -1
- package/dist/commands/project/listprojects.spec.js +111 -0
- package/dist/commands/project/listprojects.spec.js.map +1 -0
- package/dist/commands/project/setproject.spec.js +58 -0
- package/dist/commands/project/setproject.spec.js.map +1 -0
- package/dist/commands/project/updateproject.js +68 -0
- package/dist/commands/project/updateproject.js.map +1 -0
- package/dist/commands/project/updateproject.spec.js +129 -0
- package/dist/commands/project/updateproject.spec.js.map +1 -0
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/update.js +13 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/version/listversions.spec.js +121 -0
- package/dist/commands/version/listversions.spec.js.map +1 -0
- package/dist/common/UserGroupId.spec.js +92 -0
- package/dist/common/UserGroupId.spec.js.map +1 -0
- package/dist/constants.js +49 -13
- package/dist/constants.js.map +1 -1
- package/dist/index.js +40 -18
- package/dist/index.js.map +1 -1
- package/dist/logger.spec.js +125 -0
- package/dist/logger.spec.js.map +1 -0
- package/dist/polyfills/file.polyfill.js.map +1 -1
- package/dist/utils.js +59 -0
- package/dist/utils.js.map +1 -1
- package/dist/utils.spec.js +158 -0
- package/dist/utils.spec.js.map +1 -0
- package/docker/Dockerfile.ci +11 -3
- package/eslint.config.mjs +45 -0
- package/generate-client.sh +3 -0
- package/package.json +24 -16
- package/src/client/.openapi-generator/FILES +1 -0
- package/src/client/api.ts +1347 -161
- package/src/clientconfiguration.spec.ts +394 -20
- package/src/clientconfiguration.ts +74 -15
- package/src/commands/add.ts +15 -6
- package/src/commands/connection/createconnections.spec.ts +403 -0
- package/src/commands/connection/createconnections.ts +30 -5
- package/src/commands/connection/deleteconnection.spec.ts +189 -0
- package/src/commands/connection/listconnections.spec.ts +132 -0
- package/src/commands/connection/listconnections.ts +22 -3
- package/src/commands/group/addmember.spec.ts +257 -0
- package/src/commands/group/addmember.ts +2 -0
- package/src/commands/group/creategroup.spec.ts +146 -0
- package/src/commands/group/deletegroup.spec.ts +120 -0
- package/src/commands/group/getgroup.spec.ts +129 -0
- package/src/commands/group/listgroups.spec.ts +137 -0
- package/src/commands/group/removemember.spec.ts +224 -0
- package/src/commands/group/removemember.ts +1 -0
- package/src/commands/groupaccesstoken/createToken.spec.ts +211 -0
- package/src/commands/groupaccesstoken/createToken.ts +55 -51
- package/src/commands/groupaccesstoken/deleteToken.spec.ts +147 -0
- package/src/commands/groupaccesstoken/deleteToken.ts +16 -11
- package/src/commands/groupaccesstoken/groupContainer.ts +51 -51
- package/src/commands/groupaccesstoken/listTokens.spec.ts +147 -0
- package/src/commands/groupaccesstoken/listTokens.ts +16 -11
- package/src/commands/list.ts +50 -9
- package/src/commands/misc/login.spec.ts +91 -6
- package/src/commands/misc/login.ts +208 -25
- package/src/commands/misc/logout.spec.ts +32 -0
- package/src/commands/misc/loopbackOAuth.ts +127 -0
- package/src/commands/misc/set-access-token.spec.ts +95 -0
- package/src/commands/misc/set-access-token.ts +16 -16
- package/src/commands/package/deletepackage.spec.ts +164 -0
- package/src/commands/package/listpackages.spec.ts +147 -0
- package/src/commands/package/listpackages.ts +28 -5
- package/src/commands/package/publish.spec.ts +597 -0
- package/src/commands/package/publish.ts +32 -0
- package/src/commands/package/updatepackage.spec.ts +98 -0
- package/src/commands/project/createproject.spec.ts +247 -0
- package/src/commands/project/createproject.ts +12 -1
- package/src/commands/project/deleteproject.spec.ts +123 -0
- package/src/commands/project/getproject.spec.ts +109 -0
- package/src/commands/project/listprojects.spec.ts +121 -0
- package/src/commands/project/listprojects.ts +33 -7
- package/src/commands/project/setproject.spec.ts +63 -0
- package/src/commands/project/updateproject.spec.ts +217 -0
- package/src/commands/project/updateproject.ts +82 -0
- package/src/commands/rm.ts +1 -6
- package/src/commands/update.ts +24 -1
- package/src/commands/version/listversions.spec.ts +138 -0
- package/src/common/UserGroupId.spec.ts +117 -0
- package/src/constants.ts +59 -13
- package/src/index.ts +46 -20
- package/src/logger.spec.ts +158 -0
- package/src/polyfills/file.polyfill.ts +1 -0
- package/src/utils.spec.ts +135 -0
- package/src/utils.ts +81 -0
- package/tsconfig.json +15 -18
- package/.eslintrc +0 -33
|
@@ -1,28 +1,210 @@
|
|
|
1
1
|
import axios, { isAxiosError } from "axios";
|
|
2
2
|
import { jwtDecode } from "jwt-decode";
|
|
3
|
+
import { URLSearchParams } from "url";
|
|
4
|
+
import { OrganizationsApi } from "../../client/api";
|
|
5
|
+
import { Configuration } from "../../client/configuration";
|
|
3
6
|
import { ClientConfiguration } from "../../clientconfiguration";
|
|
4
7
|
import {
|
|
5
8
|
auth0ConfigsByEnv,
|
|
9
|
+
AUTH_CLI_LOOPBACK_REDIRECT_URI,
|
|
6
10
|
Environment,
|
|
11
|
+
getAuth0OrganizationIdForCliLogin,
|
|
7
12
|
LOGIN_TIMEOUT_MS,
|
|
8
13
|
} from "../../constants";
|
|
9
14
|
import { logger } from "../../logger";
|
|
10
|
-
import {
|
|
15
|
+
import { printOutError, sleep } from "../../utils";
|
|
16
|
+
import * as loopbackOAuth from "./loopbackOAuth";
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
const organizationsApi = new OrganizationsApi(new Configuration());
|
|
19
|
+
|
|
20
|
+
export async function login(
|
|
21
|
+
organizationName: string,
|
|
22
|
+
controlPlaneBaseURL?: string,
|
|
23
|
+
configPath?: string,
|
|
24
|
+
) {
|
|
13
25
|
const config = new ClientConfiguration(configPath);
|
|
14
26
|
const environment = config.getEnvironment();
|
|
15
27
|
|
|
16
28
|
if (environment === "e2e-test") {
|
|
17
29
|
return loginM2M(organizationName, config);
|
|
18
|
-
}
|
|
19
|
-
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const auth0OrganizationId =
|
|
33
|
+
getAuth0OrganizationIdForCliLogin(organizationName);
|
|
34
|
+
if (auth0OrganizationId) {
|
|
35
|
+
return loginWithAuthCodePkceOrganization(
|
|
36
|
+
environment,
|
|
37
|
+
organizationName,
|
|
38
|
+
controlPlaneBaseURL,
|
|
39
|
+
config,
|
|
40
|
+
auth0OrganizationId,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return loginWithDeviceCode(
|
|
45
|
+
environment,
|
|
46
|
+
organizationName,
|
|
47
|
+
controlPlaneBaseURL,
|
|
48
|
+
config,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function persistTokensAndVerifyOrganization(
|
|
53
|
+
tokenPayload: {
|
|
54
|
+
id_token?: string;
|
|
55
|
+
access_token: string;
|
|
56
|
+
refresh_token?: string;
|
|
57
|
+
},
|
|
58
|
+
organizationName: string,
|
|
59
|
+
controlPlaneBaseURL: string,
|
|
60
|
+
config: ClientConfiguration,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
if (tokenPayload.id_token) {
|
|
63
|
+
const decoded = jwtDecode(tokenPayload.id_token);
|
|
64
|
+
if (
|
|
65
|
+
typeof decoded === "object" &&
|
|
66
|
+
decoded !== null &&
|
|
67
|
+
"email" in decoded &&
|
|
68
|
+
"name" in decoded
|
|
69
|
+
) {
|
|
70
|
+
const user = decoded as { email?: string; name?: string };
|
|
71
|
+
config.setVSCodeUser(user.name, user.email);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
config.setAccessToken(true, tokenPayload.access_token);
|
|
76
|
+
config.setJwtAccessToken(tokenPayload.access_token);
|
|
77
|
+
if (tokenPayload.refresh_token) {
|
|
78
|
+
config.setRefreshToken(tokenPayload.refresh_token);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await organizationsApi.getOrganization(
|
|
83
|
+
organizationName,
|
|
84
|
+
{
|
|
85
|
+
baseURL: controlPlaneBaseURL,
|
|
86
|
+
headers: config.getHeaders(),
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (response.status === 200) {
|
|
91
|
+
if (organizationName !== config.getOrganization()) {
|
|
92
|
+
config.setOrganization(organizationName);
|
|
93
|
+
config.setProject(undefined);
|
|
94
|
+
}
|
|
95
|
+
logger.info("Login successful");
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!controlPlaneBaseURL.startsWith("https://")) {
|
|
99
|
+
const errorMsg =
|
|
100
|
+
error instanceof Error ? error.message : String(error);
|
|
101
|
+
logger.warn(
|
|
102
|
+
"Error with organziation check- might be bootstrapping. Ignoring: " +
|
|
103
|
+
errorMsg,
|
|
104
|
+
);
|
|
105
|
+
config.setOrganization(organizationName);
|
|
106
|
+
config.setProject(undefined);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isAxiosError(error)) {
|
|
111
|
+
if (error.response?.status === 403 || error.response?.status === 404) {
|
|
112
|
+
logger.error(
|
|
113
|
+
`The organization "${organizationName}" does not exist, or you do not have permission to access it.`,
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function loginWithAuthCodePkceOrganization(
|
|
123
|
+
environment: Environment,
|
|
124
|
+
organizationName: string,
|
|
125
|
+
controlPlaneBaseURL: string,
|
|
126
|
+
config: ClientConfiguration,
|
|
127
|
+
auth0OrganizationId: string,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
const auth0Domain = auth0ConfigsByEnv[environment].domain;
|
|
130
|
+
const auth0ClientId = auth0ConfigsByEnv[environment].clientId;
|
|
131
|
+
const audience = auth0ConfigsByEnv[environment].audience;
|
|
132
|
+
|
|
133
|
+
const { codeVerifier, codeChallenge } = loopbackOAuth.generatePkcePair();
|
|
134
|
+
const state = loopbackOAuth.generateOAuthState();
|
|
135
|
+
|
|
136
|
+
const authorizeParams = new URLSearchParams({
|
|
137
|
+
response_type: "code",
|
|
138
|
+
client_id: auth0ClientId,
|
|
139
|
+
redirect_uri: AUTH_CLI_LOOPBACK_REDIRECT_URI,
|
|
140
|
+
scope: "openid profile email offline_access",
|
|
141
|
+
audience,
|
|
142
|
+
organization: auth0OrganizationId,
|
|
143
|
+
code_challenge: codeChallenge,
|
|
144
|
+
code_challenge_method: "S256",
|
|
145
|
+
state,
|
|
146
|
+
});
|
|
147
|
+
const authorizeUrl = `${auth0Domain}/authorize?${authorizeParams.toString()}`;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const { code } = await loopbackOAuth.authorizationCodeViaLoopback({
|
|
151
|
+
redirectUri: AUTH_CLI_LOOPBACK_REDIRECT_URI,
|
|
152
|
+
expectedState: state,
|
|
153
|
+
authorizeUrl,
|
|
154
|
+
timeoutMs: LOGIN_TIMEOUT_MS,
|
|
155
|
+
onListening: () => {
|
|
156
|
+
logger.info(
|
|
157
|
+
`Please visit the following URL to login ${authorizeUrl}`,
|
|
158
|
+
);
|
|
159
|
+
logger.info("Waiting for login...");
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const tokenResponse = await axios.request({
|
|
164
|
+
method: "POST",
|
|
165
|
+
url: `${auth0Domain}/oauth/token`,
|
|
166
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
167
|
+
data: new URLSearchParams({
|
|
168
|
+
grant_type: "authorization_code",
|
|
169
|
+
client_id: auth0ClientId,
|
|
170
|
+
code,
|
|
171
|
+
redirect_uri: AUTH_CLI_LOOPBACK_REDIRECT_URI,
|
|
172
|
+
code_verifier: codeVerifier,
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await persistTokensAndVerifyOrganization(
|
|
177
|
+
tokenResponse.data,
|
|
178
|
+
organizationName,
|
|
179
|
+
controlPlaneBaseURL,
|
|
180
|
+
config,
|
|
181
|
+
);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (isAxiosError(e)) {
|
|
184
|
+
const d = e.response?.data;
|
|
185
|
+
if (
|
|
186
|
+
typeof d === "object" &&
|
|
187
|
+
d !== null &&
|
|
188
|
+
"error" in d &&
|
|
189
|
+
d.error === "redirect_uri_mismatch"
|
|
190
|
+
) {
|
|
191
|
+
logger.error(
|
|
192
|
+
`Auth0 rejected the CLI callback URL. Add ${AUTH_CLI_LOOPBACK_REDIRECT_URI} to this application's Allowed Callback URLs in Auth0.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
printOutError(e, false);
|
|
196
|
+
} else {
|
|
197
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
198
|
+
logger.error(msg);
|
|
199
|
+
}
|
|
200
|
+
process.exit(1);
|
|
20
201
|
}
|
|
21
202
|
}
|
|
22
203
|
|
|
23
204
|
async function loginWithDeviceCode(
|
|
24
205
|
environment: Environment,
|
|
25
206
|
organizationName: string,
|
|
207
|
+
controlPlaneBaseURL: string,
|
|
26
208
|
config: ClientConfiguration,
|
|
27
209
|
) {
|
|
28
210
|
const auth0Domain = auth0ConfigsByEnv[environment].domain;
|
|
@@ -65,33 +247,34 @@ async function loginWithDeviceCode(
|
|
|
65
247
|
client_id: auth0ClientId,
|
|
66
248
|
}),
|
|
67
249
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
) {
|
|
75
|
-
const user = decoded as { email?: string; name?: string };
|
|
76
|
-
config.setVSCodeUser(user.name, user.email);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
config.setAccessToken(true, accessToken.data.access_token);
|
|
80
|
-
config.setJwtAccessToken(accessToken.data.access_token);
|
|
81
|
-
config.setRefreshToken(accessToken.data.refresh_token);
|
|
82
|
-
if (organizationName !== config.getOrganization()) {
|
|
83
|
-
config.setOrganization(organizationName);
|
|
84
|
-
config.setProject(undefined);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
logger.info("Login successful");
|
|
250
|
+
await persistTokensAndVerifyOrganization(
|
|
251
|
+
accessToken.data,
|
|
252
|
+
organizationName,
|
|
253
|
+
controlPlaneBaseURL,
|
|
254
|
+
config,
|
|
255
|
+
);
|
|
88
256
|
break;
|
|
89
257
|
} catch (error) {
|
|
90
258
|
if (
|
|
91
259
|
isAxiosError(error) &&
|
|
92
260
|
error.response?.data?.error !== "authorization_pending"
|
|
93
261
|
) {
|
|
94
|
-
|
|
262
|
+
const errorData = error.response?.data;
|
|
263
|
+
if (errorData) {
|
|
264
|
+
if (typeof errorData === "string") {
|
|
265
|
+
logger.error(errorData);
|
|
266
|
+
} else if (errorData.error_description) {
|
|
267
|
+
logger.error(errorData.error_description);
|
|
268
|
+
} else if (errorData.error) {
|
|
269
|
+
logger.error(errorData.error);
|
|
270
|
+
} else {
|
|
271
|
+
logger.error(JSON.stringify(errorData));
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
const errorMsg =
|
|
275
|
+
error instanceof Error ? error.message : String(error);
|
|
276
|
+
logger.error(errorMsg || "Unknown error occurred");
|
|
277
|
+
}
|
|
95
278
|
}
|
|
96
279
|
if (Date.now() - startTime > LOGIN_TIMEOUT_MS) {
|
|
97
280
|
logger.info("Login timed out after 5 minutes");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import sinon from "sinon";
|
|
3
|
+
import { ClientConfiguration } from "../../clientconfiguration";
|
|
4
|
+
import { logger } from "../../logger";
|
|
5
|
+
import { logout } from "./logout";
|
|
6
|
+
|
|
7
|
+
describe("logout", () => {
|
|
8
|
+
let sandbox: sinon.SinonSandbox;
|
|
9
|
+
let destroyConfigStub: sinon.SinonStub;
|
|
10
|
+
let loggerStub: sinon.SinonStubbedInstance<typeof logger>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
sandbox = sinon.createSandbox();
|
|
14
|
+
loggerStub = sandbox.stub(logger);
|
|
15
|
+
destroyConfigStub = sandbox.stub(
|
|
16
|
+
ClientConfiguration.prototype,
|
|
17
|
+
"destroyConfig",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
sandbox.restore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should destroy config and log success message", () => {
|
|
26
|
+
logout();
|
|
27
|
+
|
|
28
|
+
expect(destroyConfigStub.calledOnce).to.be.true;
|
|
29
|
+
expect(loggerStub.info.calledWith(sinon.match("Credentials reset"))).to.be
|
|
30
|
+
.true;
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "crypto";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import { URL } from "url";
|
|
4
|
+
|
|
5
|
+
export function generatePkcePair(): {
|
|
6
|
+
codeVerifier: string;
|
|
7
|
+
codeChallenge: string;
|
|
8
|
+
} {
|
|
9
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
10
|
+
const codeChallenge = createHash("sha256")
|
|
11
|
+
.update(codeVerifier)
|
|
12
|
+
.digest("base64url");
|
|
13
|
+
return { codeVerifier, codeChallenge };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function generateOAuthState(): string {
|
|
17
|
+
return randomBytes(24).toString("base64url");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Listens on loopback `redirectUri` for the OAuth redirect, then exchanges are done by the caller.
|
|
22
|
+
* Caller should print `authorizeUrl` as plain text (e.g. in {@link onListening}) — no browser auto-open.
|
|
23
|
+
* Used when the CLI must send `organization` on `/authorize` (Auth0 Organizations); the documented
|
|
24
|
+
* device-code endpoint does not list an `organization` parameter
|
|
25
|
+
* (https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow/call-your-api-using-the-device-authorization-flow#device-code-parameters).
|
|
26
|
+
*/
|
|
27
|
+
export async function authorizationCodeViaLoopback(options: {
|
|
28
|
+
redirectUri: string;
|
|
29
|
+
expectedState: string;
|
|
30
|
+
authorizeUrl: string;
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
/** Invoked once the loopback server is ready (safe to tell the user to open the URL). */
|
|
33
|
+
onListening?: () => void;
|
|
34
|
+
}): Promise<{ code: string }> {
|
|
35
|
+
const {
|
|
36
|
+
redirectUri,
|
|
37
|
+
expectedState,
|
|
38
|
+
authorizeUrl: _authorizeUrl,
|
|
39
|
+
timeoutMs,
|
|
40
|
+
onListening,
|
|
41
|
+
} = options;
|
|
42
|
+
const parsed = new URL(redirectUri);
|
|
43
|
+
const port = Number(parsed.port);
|
|
44
|
+
const pathname = parsed.pathname;
|
|
45
|
+
|
|
46
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid redirect URI (need explicit port): ${redirectUri}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
let settled = false;
|
|
54
|
+
// eslint-disable-next-line prefer-const -- assign after finish/timer (handler may run before `const` bindings exist)
|
|
55
|
+
let server: http.Server | undefined;
|
|
56
|
+
|
|
57
|
+
const finish = (err: Error | null, result?: { code: string }) => {
|
|
58
|
+
if (settled) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
settled = true;
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
server?.close();
|
|
64
|
+
if (err) {
|
|
65
|
+
reject(err);
|
|
66
|
+
} else {
|
|
67
|
+
resolve(result!);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
finish(
|
|
73
|
+
new Error(
|
|
74
|
+
`Login timed out after ${Math.round(timeoutMs / 1000)} seconds`,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
|
|
79
|
+
server = http.createServer((req, res) => {
|
|
80
|
+
if (settled || !req.url) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const reqUrl = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
85
|
+
if (reqUrl.pathname !== pathname) {
|
|
86
|
+
res.writeHead(404);
|
|
87
|
+
res.end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const code = reqUrl.searchParams.get("code");
|
|
91
|
+
const state = reqUrl.searchParams.get("state");
|
|
92
|
+
const error = reqUrl.searchParams.get("error");
|
|
93
|
+
|
|
94
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
95
|
+
res.end(
|
|
96
|
+
"<!DOCTYPE html><html><body><p>You can close this tab and return to the terminal.</p></body></html>",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (error) {
|
|
100
|
+
finish(
|
|
101
|
+
new Error(
|
|
102
|
+
reqUrl.searchParams.get("error_description") || error,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!code || state !== expectedState) {
|
|
108
|
+
finish(new Error("Invalid authorization response"));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
finish(null, { code });
|
|
112
|
+
} catch (e) {
|
|
113
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
server.on("error", (err) => finish(err));
|
|
118
|
+
|
|
119
|
+
server.listen(port, "127.0.0.1", () => {
|
|
120
|
+
try {
|
|
121
|
+
onListening?.();
|
|
122
|
+
} catch (e) {
|
|
123
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import sinon from "sinon";
|
|
3
|
+
import { ClientConfiguration } from "../../clientconfiguration";
|
|
4
|
+
import { logger } from "../../logger";
|
|
5
|
+
import { setAccessToken } from "./set-access-token";
|
|
6
|
+
|
|
7
|
+
describe("set-access-token", () => {
|
|
8
|
+
let sandbox: sinon.SinonSandbox;
|
|
9
|
+
let setAccessTokenStub: sinon.SinonStub;
|
|
10
|
+
let setJwtAccessTokenStub: sinon.SinonStub;
|
|
11
|
+
let setOrganizationStub: sinon.SinonStub;
|
|
12
|
+
let loggerStub: sinon.SinonStubbedInstance<typeof logger>;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
sandbox = sinon.createSandbox();
|
|
16
|
+
loggerStub = sandbox.stub(logger);
|
|
17
|
+
setAccessTokenStub = sandbox.stub(
|
|
18
|
+
ClientConfiguration.prototype,
|
|
19
|
+
"setAccessToken",
|
|
20
|
+
);
|
|
21
|
+
setJwtAccessTokenStub = sandbox.stub(
|
|
22
|
+
ClientConfiguration.prototype,
|
|
23
|
+
"setJwtAccessToken",
|
|
24
|
+
);
|
|
25
|
+
setOrganizationStub = sandbox.stub(
|
|
26
|
+
ClientConfiguration.prototype,
|
|
27
|
+
"setOrganization",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
sandbox.restore();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should set access token successfully", () => {
|
|
36
|
+
const jwtToken = "test-jwt-token";
|
|
37
|
+
|
|
38
|
+
setAccessToken(jwtToken);
|
|
39
|
+
|
|
40
|
+
expect(setAccessTokenStub.calledOnce).to.be.true;
|
|
41
|
+
expect(setAccessTokenStub.firstCall.args).to.deep.equal([
|
|
42
|
+
false,
|
|
43
|
+
jwtToken,
|
|
44
|
+
]);
|
|
45
|
+
expect(setJwtAccessTokenStub.calledOnce).to.be.true;
|
|
46
|
+
expect(setJwtAccessTokenStub.firstCall.args[0]).to.equal(jwtToken);
|
|
47
|
+
expect(
|
|
48
|
+
loggerStub.info.calledWith(
|
|
49
|
+
sinon.match("Access token set successfully"),
|
|
50
|
+
),
|
|
51
|
+
).to.be.true;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should set access token and organization when provided", () => {
|
|
55
|
+
const jwtToken = "test-jwt-token";
|
|
56
|
+
const organizationName = "test-org";
|
|
57
|
+
|
|
58
|
+
setAccessToken(jwtToken, organizationName);
|
|
59
|
+
|
|
60
|
+
expect(setAccessTokenStub.calledOnce).to.be.true;
|
|
61
|
+
expect(setJwtAccessTokenStub.calledOnce).to.be.true;
|
|
62
|
+
expect(setOrganizationStub.calledOnce).to.be.true;
|
|
63
|
+
expect(setOrganizationStub.firstCall.args[0]).to.equal(organizationName);
|
|
64
|
+
expect(
|
|
65
|
+
loggerStub.info.calledWith(
|
|
66
|
+
sinon.match("Access token set successfully"),
|
|
67
|
+
),
|
|
68
|
+
).to.be.true;
|
|
69
|
+
expect(
|
|
70
|
+
loggerStub.info.calledWith(
|
|
71
|
+
sinon.match(`Organization set to: ${organizationName}`),
|
|
72
|
+
),
|
|
73
|
+
).to.be.true;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should not set organization when not provided", () => {
|
|
77
|
+
const jwtToken = "test-jwt-token";
|
|
78
|
+
|
|
79
|
+
setAccessToken(jwtToken);
|
|
80
|
+
|
|
81
|
+
expect(setOrganizationStub.called).to.be.false;
|
|
82
|
+
expect(loggerStub.info.calledWith(sinon.match(/Organization set to:/))).to
|
|
83
|
+
.be.false;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should handle custom config path", () => {
|
|
87
|
+
const jwtToken = "test-jwt-token";
|
|
88
|
+
const configPath = "/custom/path/config.json";
|
|
89
|
+
|
|
90
|
+
setAccessToken(jwtToken, undefined, configPath);
|
|
91
|
+
|
|
92
|
+
expect(setAccessTokenStub.calledOnce).to.be.true;
|
|
93
|
+
expect(setJwtAccessTokenStub.calledOnce).to.be.true;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -3,30 +3,30 @@ import { logger } from "../../logger";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Sets a service account JWT token for CLI authentication.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @param jwtToken The JWT token to use for authentication
|
|
8
8
|
* @param organizationName Optional organization name to set
|
|
9
9
|
* @param configPath Optional custom config path
|
|
10
10
|
*/
|
|
11
11
|
export function setAccessToken(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
jwtToken: string,
|
|
13
|
+
organizationName?: string,
|
|
14
|
+
configPath?: string,
|
|
15
15
|
) {
|
|
16
|
-
|
|
16
|
+
const config = new ClientConfiguration(configPath);
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Set the JWT token as both access token and JWT access token
|
|
19
|
+
config.setAccessToken(false, jwtToken);
|
|
20
|
+
config.setJwtAccessToken(jwtToken);
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// Set organization if provided
|
|
23
|
+
if (organizationName) {
|
|
24
|
+
config.setOrganization(organizationName);
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
logger.info("Access token set successfully");
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (organizationName) {
|
|
30
|
+
logger.info(`Organization set to: ${organizationName}`);
|
|
31
|
+
}
|
|
32
32
|
}
|