@sasjs/cli 6.2.7 → 6.3.0
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/build/commands/auth/authCommand.js +5 -3
- package/build/commands/auth/login.d.ts +9 -3
- package/build/commands/auth/login.js +40 -21
- package/build/commands/auth/spec/login.spec.js +103 -3
- package/build/utils/auth.d.ts +30 -0
- package/build/utils/auth.js +230 -0
- package/build/utils/config.js +2 -2
- package/build/utils/spec/sasjsLogin.spec.d.ts +1 -0
- package/build/utils/spec/sasjsLogin.spec.js +370 -0
- package/package.json +1 -1
|
@@ -68,15 +68,17 @@ var syntax = 'auth [subCommand] [options]';
|
|
|
68
68
|
var usage = 'sasjs auth login --target <target-name> | sasjs auth [options]';
|
|
69
69
|
var description = "Authenticates against the specified target.\n" +
|
|
70
70
|
"With the 'login' subCommand, authentication is performed with a SAS username and password " +
|
|
71
|
-
"(no OAuth client/secret required
|
|
71
|
+
"(no OAuth client/secret required) and the resulting tokens are saved. " +
|
|
72
|
+
"Supported on SASVIYA targets, and on SASJS targets, where the server's own local login is used.\n" +
|
|
72
73
|
"Without a subCommand, behaves like 'sasjs add cred' (client/secret based authentication).\n" +
|
|
73
74
|
"\n" +
|
|
74
75
|
"Non-interactive use (CI/agents): set SAS_USERNAME and SAS_PASSWORD environment variables, " +
|
|
75
|
-
"or pipe the password and use --password-stdin."
|
|
76
|
+
"or pipe the password and use --password-stdin. On a SASJS target, set SASJS_CLIENT to the " +
|
|
77
|
+
"registered client id when the account is not an administrator.";
|
|
76
78
|
var examples = [
|
|
77
79
|
{
|
|
78
80
|
command: 'sasjs auth login --target <target-name>',
|
|
79
|
-
description: 'Logs in with SAS username/password (no client/secret required).'
|
|
81
|
+
description: 'Logs in with SAS username/password (no client/secret required) on a SASVIYA or SASJS target.'
|
|
80
82
|
},
|
|
81
83
|
{
|
|
82
84
|
command: 'sasjs auth login -t <target-name>',
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { Target } from '@sasjs/utils';
|
|
2
2
|
/**
|
|
3
|
-
* Authenticates against a SASVIYA target using a SAS username and
|
|
4
|
-
*
|
|
5
|
-
* `sas.cli` client) and persists the resulting token pair. This enables all
|
|
3
|
+
* Authenticates against a SASVIYA or SASJS target using a SAS username and
|
|
4
|
+
* password, and persists the resulting token pair. This enables all
|
|
6
5
|
* authenticated commands (`sasjs run`, `sasjs deploy`, etc.) on estates where
|
|
7
6
|
* no administrator-registered OAuth client/secret is available.
|
|
8
7
|
*
|
|
8
|
+
* The two server types issue tokens differently: SASVIYA uses the OAuth2
|
|
9
|
+
* resource owner password grant against the built-in, secret-less `sas.cli`
|
|
10
|
+
* client, while SASJS has no password grant and is authenticated through the
|
|
11
|
+
* server's own local login (@see getTokensWithSasjsLogin). Everything else -
|
|
12
|
+
* how the credentials are obtained, and the rule that nothing is prompted for
|
|
13
|
+
* when there is no TTY - is shared by both.
|
|
14
|
+
*
|
|
9
15
|
* Credentials are resolved in this order (highest precedence first):
|
|
10
16
|
*
|
|
11
17
|
* Username: SAS_USERNAME env var > interactive prompt (TTY only)
|
|
@@ -139,12 +139,18 @@ var readPasswordFromStdin = function () { return __awaiter(void 0, void 0, void
|
|
|
139
139
|
});
|
|
140
140
|
}); };
|
|
141
141
|
/**
|
|
142
|
-
* Authenticates against a SASVIYA target using a SAS username and
|
|
143
|
-
*
|
|
144
|
-
* `sas.cli` client) and persists the resulting token pair. This enables all
|
|
142
|
+
* Authenticates against a SASVIYA or SASJS target using a SAS username and
|
|
143
|
+
* password, and persists the resulting token pair. This enables all
|
|
145
144
|
* authenticated commands (`sasjs run`, `sasjs deploy`, etc.) on estates where
|
|
146
145
|
* no administrator-registered OAuth client/secret is available.
|
|
147
146
|
*
|
|
147
|
+
* The two server types issue tokens differently: SASVIYA uses the OAuth2
|
|
148
|
+
* resource owner password grant against the built-in, secret-less `sas.cli`
|
|
149
|
+
* client, while SASJS has no password grant and is authenticated through the
|
|
150
|
+
* server's own local login (@see getTokensWithSasjsLogin). Everything else -
|
|
151
|
+
* how the credentials are obtained, and the rule that nothing is prompted for
|
|
152
|
+
* when there is no TTY - is shared by both.
|
|
153
|
+
*
|
|
148
154
|
* Credentials are resolved in this order (highest precedence first):
|
|
149
155
|
*
|
|
150
156
|
* Username: SAS_USERNAME env var > interactive prompt (TTY only)
|
|
@@ -175,15 +181,16 @@ var authLogin = function (target_1) {
|
|
|
175
181
|
args_1[_i - 1] = arguments[_i];
|
|
176
182
|
}
|
|
177
183
|
return __awaiter(void 0, __spreadArray([target_1], __read(args_1), false), void 0, function (target, insecure, passwordStdin) {
|
|
178
|
-
var isTty, envUser, envPass, user, hint, pass, promptedPass, _a, access_token, refresh_token,
|
|
179
|
-
var
|
|
184
|
+
var isTty, envUser, envPass, user, hint, pass, promptedPass, _a, access_token_1, refresh_token_1, client, displayName, _b, access_token, refresh_token, _c, id, name;
|
|
185
|
+
var _d, _e, _f, _g, _h;
|
|
180
186
|
if (insecure === void 0) { insecure = false; }
|
|
181
187
|
if (passwordStdin === void 0) { passwordStdin = false; }
|
|
182
|
-
return __generator(this, function (
|
|
183
|
-
switch (
|
|
188
|
+
return __generator(this, function (_j) {
|
|
189
|
+
switch (_j.label) {
|
|
184
190
|
case 0:
|
|
185
|
-
if (target.serverType !== utils_1.ServerType.SasViya
|
|
186
|
-
|
|
191
|
+
if (target.serverType !== utils_1.ServerType.SasViya &&
|
|
192
|
+
target.serverType !== utils_1.ServerType.Sasjs) {
|
|
193
|
+
throw new Error("'sasjs auth login' is supported for SASVIYA and SASJS targets. " +
|
|
187
194
|
"Target '".concat(target.name, "' is of type '").concat(target.serverType, "'. ") +
|
|
188
195
|
"Use 'sasjs add cred -t ".concat(target.name, "' instead."));
|
|
189
196
|
}
|
|
@@ -192,7 +199,7 @@ var authLogin = function (target_1) {
|
|
|
192
199
|
}
|
|
193
200
|
if (insecure) {
|
|
194
201
|
target = new utils_1.Target(__assign(__assign({}, target.toJson(false)), { httpsAgentOptions: __assign(__assign({}, target.httpsAgentOptions), { allowInsecureRequests: true, rejectUnauthorized: false }) }));
|
|
195
|
-
(
|
|
202
|
+
(_d = process.logger) === null || _d === void 0 ? void 0 : _d.warn('Executing with insecure connection.');
|
|
196
203
|
}
|
|
197
204
|
isTty = !!process.stdin.isTTY;
|
|
198
205
|
envUser = process.env.SAS_USERNAME;
|
|
@@ -204,7 +211,7 @@ var authLogin = function (target_1) {
|
|
|
204
211
|
if (!(isTty && !passwordStdin)) return [3 /*break*/, 3];
|
|
205
212
|
return [4 /*yield*/, (0, utils_1.getString)('Please enter your SAS username', function (v) { return !!v || 'Username is required.'; })];
|
|
206
213
|
case 2:
|
|
207
|
-
user =
|
|
214
|
+
user = _j.sent();
|
|
208
215
|
return [3 /*break*/, 4];
|
|
209
216
|
case 3:
|
|
210
217
|
hint = passwordStdin
|
|
@@ -215,7 +222,7 @@ var authLogin = function (target_1) {
|
|
|
215
222
|
if (!passwordStdin) return [3 /*break*/, 6];
|
|
216
223
|
return [4 /*yield*/, readPasswordFromStdin()];
|
|
217
224
|
case 5:
|
|
218
|
-
pass =
|
|
225
|
+
pass = _j.sent();
|
|
219
226
|
if (!pass) {
|
|
220
227
|
throw new Error("--password-stdin was specified but no password was read from stdin. " +
|
|
221
228
|
"Pipe the password, e.g. echo \"$SAS_PASSWORD\" | sasjs auth login --password-stdin -t ".concat(target.name, "."));
|
|
@@ -237,23 +244,35 @@ var authLogin = function (target_1) {
|
|
|
237
244
|
}
|
|
238
245
|
})];
|
|
239
246
|
case 8:
|
|
240
|
-
promptedPass = (
|
|
247
|
+
promptedPass = (_j.sent()).pass;
|
|
241
248
|
pass = promptedPass;
|
|
242
249
|
return [3 /*break*/, 10];
|
|
243
250
|
case 9: throw new Error("A SAS password is required but was not provided. " +
|
|
244
251
|
"Set the SAS_PASSWORD environment variable, or pipe the password and use --password-stdin " +
|
|
245
252
|
"(e.g. echo \"$SAS_PASSWORD\" | sasjs auth login --password-stdin -t ".concat(target.name, ")."));
|
|
246
|
-
case 10:
|
|
253
|
+
case 10:
|
|
254
|
+
if (!(target.serverType === utils_1.ServerType.Sasjs)) return [3 /*break*/, 13];
|
|
255
|
+
return [4 /*yield*/, (0, utils_2.getTokensWithSasjsLogin)(target, user, pass, process.env.SASJS_CLIENT)];
|
|
247
256
|
case 11:
|
|
248
|
-
_a =
|
|
249
|
-
return [4 /*yield*/, (0, utils_2.
|
|
257
|
+
_a = _j.sent(), access_token_1 = _a.access_token, refresh_token_1 = _a.refresh_token, client = _a.client, displayName = _a.displayName;
|
|
258
|
+
return [4 /*yield*/, (0, utils_2.saveTokens)(target.name, access_token_1, refresh_token_1, client)];
|
|
250
259
|
case 12:
|
|
251
|
-
|
|
260
|
+
_j.sent();
|
|
261
|
+
(_e = process.logger) === null || _e === void 0 ? void 0 : _e.success("Logged in as ".concat(displayName || user, " on ").concat(target.serverUrl, "."));
|
|
262
|
+
(_f = process.logger) === null || _f === void 0 ? void 0 : _f.info("The access token is valid for the duration configured on the server " +
|
|
263
|
+
"(24 hours by default). When it expires, re-run 'sasjs auth login -t ".concat(target.name, "'."));
|
|
264
|
+
return [2 /*return*/];
|
|
265
|
+
case 13: return [4 /*yield*/, (0, utils_2.getTokensWithPasswordGrant)(target, user, pass)];
|
|
266
|
+
case 14:
|
|
267
|
+
_b = _j.sent(), access_token = _b.access_token, refresh_token = _b.refresh_token;
|
|
268
|
+
return [4 /*yield*/, (0, utils_2.fetchLoggedInUser)(target, access_token)];
|
|
269
|
+
case 15:
|
|
270
|
+
_c = _j.sent(), id = _c.id, name = _c.name;
|
|
252
271
|
return [4 /*yield*/, (0, utils_2.saveTokens)(target.name, access_token, refresh_token || '')];
|
|
253
|
-
case
|
|
254
|
-
|
|
255
|
-
(
|
|
256
|
-
(
|
|
272
|
+
case 16:
|
|
273
|
+
_j.sent();
|
|
274
|
+
(_g = process.logger) === null || _g === void 0 ? void 0 : _g.success("Logged in as ".concat(id || 'unknown user').concat(name ? " (".concat(name, ")") : '', " on ").concat(target.serverUrl, "."));
|
|
275
|
+
(_h = process.logger) === null || _h === void 0 ? void 0 : _h.info("The access token is valid for the duration configured on the server (12 hours by default). " +
|
|
257
276
|
"When it expires, re-run 'sasjs auth login -t ".concat(target.name, "'."));
|
|
258
277
|
return [2 /*return*/];
|
|
259
278
|
}
|
|
@@ -103,7 +103,7 @@ var utils_2 = require("@sasjs/utils");
|
|
|
103
103
|
// Mock the three orchestration dependencies that authLogin delegates to.
|
|
104
104
|
// We spy on the utils barrel (../../utils) which re-exports from auth.ts and
|
|
105
105
|
// config.ts, so the spies are the same references login.ts resolves at runtime.
|
|
106
|
-
jest.mock('../../../utils', function () { return (__assign(__assign({}, jest.requireActual('../../../utils')), { getTokensWithPasswordGrant: jest.fn(), fetchLoggedInUser: jest.fn(), saveTokens: jest.fn() })); });
|
|
106
|
+
jest.mock('../../../utils', function () { return (__assign(__assign({}, jest.requireActual('../../../utils')), { getTokensWithPasswordGrant: jest.fn(), getTokensWithSasjsLogin: jest.fn(), fetchLoggedInUser: jest.fn(), saveTokens: jest.fn() })); });
|
|
107
107
|
var viyaTarget = new utils_1.Target({
|
|
108
108
|
name: 'viya-test',
|
|
109
109
|
serverType: utils_1.ServerType.SasViya,
|
|
@@ -111,6 +111,12 @@ var viyaTarget = new utils_1.Target({
|
|
|
111
111
|
appLoc: '/Public/test',
|
|
112
112
|
contextName: 'test context'
|
|
113
113
|
});
|
|
114
|
+
var sasjsTarget = new utils_1.Target({
|
|
115
|
+
name: 'sasjs-test',
|
|
116
|
+
serverType: utils_1.ServerType.Sasjs,
|
|
117
|
+
serverUrl: 'https://sasjs.example.com',
|
|
118
|
+
appLoc: '/Public/app'
|
|
119
|
+
});
|
|
114
120
|
var sas9Target = new utils_1.Target({
|
|
115
121
|
name: 'sas9-test',
|
|
116
122
|
serverType: utils_1.ServerType.Sas9,
|
|
@@ -157,10 +163,10 @@ describe('authLogin', function () {
|
|
|
157
163
|
// ---------------------------------------------------------------------------
|
|
158
164
|
// Guard 1: Non-Viya rejection
|
|
159
165
|
// ---------------------------------------------------------------------------
|
|
160
|
-
it('should reject when target.serverType is
|
|
166
|
+
it('should reject when target.serverType is SAS9', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
161
167
|
return __generator(this, function (_a) {
|
|
162
168
|
switch (_a.label) {
|
|
163
|
-
case 0: return [4 /*yield*/, expect(authLoginModule.authLogin(sas9Target)).rejects.toThrow("'sasjs auth login' is
|
|
169
|
+
case 0: return [4 /*yield*/, expect(authLoginModule.authLogin(sas9Target)).rejects.toThrow("'sasjs auth login' is supported for SASVIYA and SASJS targets.")];
|
|
164
170
|
case 1:
|
|
165
171
|
_a.sent();
|
|
166
172
|
expect(utilsModule.getTokensWithPasswordGrant).not.toHaveBeenCalled();
|
|
@@ -672,4 +678,98 @@ describe('authLogin', function () {
|
|
|
672
678
|
}
|
|
673
679
|
});
|
|
674
680
|
}); });
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// SASJS: local login against a SASjs Server target
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
describe('SASJS target', function () {
|
|
685
|
+
beforeEach(function () {
|
|
686
|
+
delete process.env.SASJS_CLIENT;
|
|
687
|
+
utilsModule.getTokensWithSasjsLogin.mockResolvedValue({
|
|
688
|
+
access_token: 'sasjs-access',
|
|
689
|
+
refresh_token: 'sasjs-refresh',
|
|
690
|
+
client: 'client-id-1',
|
|
691
|
+
displayName: 'SASJS Test User'
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
afterEach(function () {
|
|
695
|
+
delete process.env.SASJS_CLIENT;
|
|
696
|
+
});
|
|
697
|
+
it('should mint tokens with the SASJS login flow instead of the password grant', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
698
|
+
return __generator(this, function (_a) {
|
|
699
|
+
switch (_a.label) {
|
|
700
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
701
|
+
case 1:
|
|
702
|
+
_a.sent();
|
|
703
|
+
expect(utilsModule.getTokensWithPasswordGrant).not.toHaveBeenCalled();
|
|
704
|
+
expect(utilsModule.getTokensWithSasjsLogin).toHaveBeenCalledWith(expect.objectContaining({ name: 'sasjs-test' }), 'test-user', 'test-pass', undefined);
|
|
705
|
+
return [2 /*return*/];
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
}); });
|
|
709
|
+
it('should persist the client alongside the tokens', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
710
|
+
return __generator(this, function (_a) {
|
|
711
|
+
switch (_a.label) {
|
|
712
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
713
|
+
case 1:
|
|
714
|
+
_a.sent();
|
|
715
|
+
expect(utilsModule.saveTokens).toHaveBeenCalledWith('sasjs-test', 'sasjs-access', 'sasjs-refresh', 'client-id-1');
|
|
716
|
+
expect(utilsModule.fetchLoggedInUser).not.toHaveBeenCalled();
|
|
717
|
+
return [2 /*return*/];
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}); });
|
|
721
|
+
it('should pass SASJS_CLIENT through so CI can pin the client', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
722
|
+
return __generator(this, function (_a) {
|
|
723
|
+
switch (_a.label) {
|
|
724
|
+
case 0:
|
|
725
|
+
process.env.SASJS_CLIENT = 'pinned-client';
|
|
726
|
+
return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
727
|
+
case 1:
|
|
728
|
+
_a.sent();
|
|
729
|
+
expect(utilsModule.getTokensWithSasjsLogin).toHaveBeenCalledWith(expect.objectContaining({ name: 'sasjs-test' }), 'test-user', 'test-pass', 'pinned-client');
|
|
730
|
+
return [2 /*return*/];
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}); });
|
|
734
|
+
it('should authenticate from env vars with no TTY, as CI would', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
735
|
+
return __generator(this, function (_a) {
|
|
736
|
+
switch (_a.label) {
|
|
737
|
+
case 0:
|
|
738
|
+
process.env.SAS_USERNAME = 'ci-user';
|
|
739
|
+
process.env.SAS_PASSWORD = 'ci-pass';
|
|
740
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
741
|
+
value: false,
|
|
742
|
+
configurable: true
|
|
743
|
+
});
|
|
744
|
+
return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
745
|
+
case 1:
|
|
746
|
+
_a.sent();
|
|
747
|
+
expect(utilsModule.getTokensWithSasjsLogin).toHaveBeenCalledWith(expect.objectContaining({ name: 'sasjs-test' }), 'ci-user', 'ci-pass', undefined);
|
|
748
|
+
expect(utils_2.getString).not.toHaveBeenCalled();
|
|
749
|
+
expect(prompts_1.default).not.toHaveBeenCalled();
|
|
750
|
+
return [2 /*return*/];
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}); });
|
|
754
|
+
it('should fall back to the username when the server reports no display name', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
755
|
+
var success;
|
|
756
|
+
return __generator(this, function (_a) {
|
|
757
|
+
switch (_a.label) {
|
|
758
|
+
case 0:
|
|
759
|
+
;
|
|
760
|
+
utilsModule.getTokensWithSasjsLogin.mockResolvedValue({
|
|
761
|
+
access_token: 'sasjs-access',
|
|
762
|
+
refresh_token: 'sasjs-refresh',
|
|
763
|
+
client: 'client-id-1'
|
|
764
|
+
});
|
|
765
|
+
success = jest.spyOn(process.logger, 'success');
|
|
766
|
+
return [4 /*yield*/, authLoginModule.authLogin(sasjsTarget)];
|
|
767
|
+
case 1:
|
|
768
|
+
_a.sent();
|
|
769
|
+
expect(success).toHaveBeenCalledWith(expect.stringContaining('Logged in as test-user on'));
|
|
770
|
+
return [2 /*return*/];
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}); });
|
|
774
|
+
});
|
|
675
775
|
});
|
package/build/utils/auth.d.ts
CHANGED
|
@@ -59,3 +59,33 @@ export declare function getNewAccessToken(sasjsInstance: SASjs, clientId: string
|
|
|
59
59
|
access_token: string;
|
|
60
60
|
refresh_token: string;
|
|
61
61
|
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Mints a token pair from the local login of a SASjs Server target.
|
|
64
|
+
*
|
|
65
|
+
* SASjs Server has no secret-less OAuth client and no password grant: the
|
|
66
|
+
* tokens it issues come from its own sign-in flow, the one its web app uses.
|
|
67
|
+
* Reproducing that flow here is what makes `sasjs auth login` usable on a SASjs
|
|
68
|
+
* target from CI, where there is no browser to visit /SASjsLogon and no
|
|
69
|
+
* administrator-registered client secret to fall back on:
|
|
70
|
+
*
|
|
71
|
+
* GET / sets the XSRF-TOKEN cookie
|
|
72
|
+
* POST /SASLogon/login creates the session
|
|
73
|
+
* POST /SASLogon/authorize returns a single-use authorization code
|
|
74
|
+
* POST /SASjsApi/auth/token exchanges it for an access/refresh pair
|
|
75
|
+
*
|
|
76
|
+
* @param {Target} target - the SASJS target to authenticate against.
|
|
77
|
+
* @param {string} user - the SASjs Server username.
|
|
78
|
+
* @param {string} pass - the SASjs Server password.
|
|
79
|
+
* @param {string} [clientId] - the registered client to authorize, usually from
|
|
80
|
+
* the SASJS_CLIENT environment variable. When absent, the target's configured
|
|
81
|
+
* client is used, and failing that the first registered client if the account
|
|
82
|
+
* is an administrator.
|
|
83
|
+
* @returns the token pair, the client that was authorized, and the display name
|
|
84
|
+
* the server reports for the user.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getTokensWithSasjsLogin(target: Target, user: string, pass: string, clientId?: string): Promise<{
|
|
87
|
+
access_token: string;
|
|
88
|
+
refresh_token: string;
|
|
89
|
+
client: string;
|
|
90
|
+
displayName?: string;
|
|
91
|
+
}>;
|
package/build/utils/auth.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
36
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
37
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -35,6 +68,34 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
68
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
69
|
}
|
|
37
70
|
};
|
|
71
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
72
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
73
|
+
if (!m) return o;
|
|
74
|
+
var i = m.call(o), r, ar = [], e;
|
|
75
|
+
try {
|
|
76
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
77
|
+
}
|
|
78
|
+
catch (error) { e = { error: error }; }
|
|
79
|
+
finally {
|
|
80
|
+
try {
|
|
81
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
82
|
+
}
|
|
83
|
+
finally { if (e) throw e.error; }
|
|
84
|
+
}
|
|
85
|
+
return ar;
|
|
86
|
+
};
|
|
87
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
88
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
89
|
+
if (ar || !(i in from)) {
|
|
90
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
91
|
+
ar[i] = from[i];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
95
|
+
};
|
|
96
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
97
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
98
|
+
};
|
|
38
99
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
100
|
exports.SAS_CLI_CLIENT_ID = exports.getAuthUrl = void 0;
|
|
40
101
|
exports.getAuthCode = getAuthCode;
|
|
@@ -42,6 +103,9 @@ exports.refreshTokens = refreshTokens;
|
|
|
42
103
|
exports.getTokensWithPasswordGrant = getTokensWithPasswordGrant;
|
|
43
104
|
exports.fetchLoggedInUser = fetchLoggedInUser;
|
|
44
105
|
exports.getNewAccessToken = getNewAccessToken;
|
|
106
|
+
exports.getTokensWithSasjsLogin = getTokensWithSasjsLogin;
|
|
107
|
+
var axios_1 = __importDefault(require("axios"));
|
|
108
|
+
var https = __importStar(require("https"));
|
|
45
109
|
var utils_1 = require("@sasjs/utils");
|
|
46
110
|
var node_1 = require("@sasjs/adapter/node");
|
|
47
111
|
/**
|
|
@@ -217,3 +281,169 @@ function getNewAccessToken(sasjsInstance, clientId, clientSecret, target) {
|
|
|
217
281
|
});
|
|
218
282
|
});
|
|
219
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Mints a token pair from the local login of a SASjs Server target.
|
|
286
|
+
*
|
|
287
|
+
* SASjs Server has no secret-less OAuth client and no password grant: the
|
|
288
|
+
* tokens it issues come from its own sign-in flow, the one its web app uses.
|
|
289
|
+
* Reproducing that flow here is what makes `sasjs auth login` usable on a SASjs
|
|
290
|
+
* target from CI, where there is no browser to visit /SASjsLogon and no
|
|
291
|
+
* administrator-registered client secret to fall back on:
|
|
292
|
+
*
|
|
293
|
+
* GET / sets the XSRF-TOKEN cookie
|
|
294
|
+
* POST /SASLogon/login creates the session
|
|
295
|
+
* POST /SASLogon/authorize returns a single-use authorization code
|
|
296
|
+
* POST /SASjsApi/auth/token exchanges it for an access/refresh pair
|
|
297
|
+
*
|
|
298
|
+
* @param {Target} target - the SASJS target to authenticate against.
|
|
299
|
+
* @param {string} user - the SASjs Server username.
|
|
300
|
+
* @param {string} pass - the SASjs Server password.
|
|
301
|
+
* @param {string} [clientId] - the registered client to authorize, usually from
|
|
302
|
+
* the SASJS_CLIENT environment variable. When absent, the target's configured
|
|
303
|
+
* client is used, and failing that the first registered client if the account
|
|
304
|
+
* is an administrator.
|
|
305
|
+
* @returns the token pair, the client that was authorized, and the display name
|
|
306
|
+
* the server reports for the user.
|
|
307
|
+
*/
|
|
308
|
+
function getTokensWithSasjsLogin(target, user, pass, clientId) {
|
|
309
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
310
|
+
var baseUrl, cookies, safeDecode, collectCookies, instance, call, fail, login, client, clients, authorized, code, tokens;
|
|
311
|
+
var _this = this;
|
|
312
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
313
|
+
return __generator(this, function (_k) {
|
|
314
|
+
switch (_k.label) {
|
|
315
|
+
case 0:
|
|
316
|
+
baseUrl = target.serverUrl.replace(/\/+$/, '');
|
|
317
|
+
cookies = new Map();
|
|
318
|
+
safeDecode = function (value) {
|
|
319
|
+
try {
|
|
320
|
+
return decodeURIComponent(value);
|
|
321
|
+
}
|
|
322
|
+
catch (_a) {
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
collectCookies = function (setCookie) {
|
|
327
|
+
var entries = Array.isArray(setCookie)
|
|
328
|
+
? setCookie
|
|
329
|
+
: setCookie
|
|
330
|
+
? [setCookie]
|
|
331
|
+
: [];
|
|
332
|
+
entries.forEach(function (entry) {
|
|
333
|
+
var pair = entry.split(';')[0];
|
|
334
|
+
var index = pair.indexOf('=');
|
|
335
|
+
if (index > 0) {
|
|
336
|
+
cookies.set(pair.slice(0, index).trim(), pair.slice(index + 1).trim());
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
instance = axios_1.default.create({
|
|
341
|
+
baseURL: baseUrl,
|
|
342
|
+
validateStatus: function () { return true; },
|
|
343
|
+
// The login request carries the password in a body, so a redirect must not
|
|
344
|
+
// replay it to wherever 'Location' points. A redirect is a configuration
|
|
345
|
+
// problem (proxy, SSO front end) - report it instead of following it.
|
|
346
|
+
maxRedirects: 0,
|
|
347
|
+
httpsAgent: target.httpsAgentOptions
|
|
348
|
+
? new https.Agent(target.httpsAgentOptions)
|
|
349
|
+
: undefined,
|
|
350
|
+
headers: { Accept: 'application/json' }
|
|
351
|
+
});
|
|
352
|
+
call = function (method, path, data) { return __awaiter(_this, void 0, void 0, function () {
|
|
353
|
+
var headers, xsrf, response;
|
|
354
|
+
return __generator(this, function (_a) {
|
|
355
|
+
switch (_a.label) {
|
|
356
|
+
case 0:
|
|
357
|
+
headers = {};
|
|
358
|
+
if (cookies.size) {
|
|
359
|
+
headers.Cookie = __spreadArray([], __read(cookies), false).map(function (_a) {
|
|
360
|
+
var _b = __read(_a, 2), name = _b[0], value = _b[1];
|
|
361
|
+
return "".concat(name, "=").concat(value);
|
|
362
|
+
})
|
|
363
|
+
.join('; ');
|
|
364
|
+
}
|
|
365
|
+
xsrf = cookies.get('XSRF-TOKEN');
|
|
366
|
+
if (xsrf)
|
|
367
|
+
headers['X-XSRF-TOKEN'] = safeDecode(xsrf);
|
|
368
|
+
return [4 /*yield*/, instance.request({
|
|
369
|
+
method: method,
|
|
370
|
+
url: path,
|
|
371
|
+
data: data,
|
|
372
|
+
headers: headers
|
|
373
|
+
})];
|
|
374
|
+
case 1:
|
|
375
|
+
response = _a.sent();
|
|
376
|
+
if (response.status >= 300 && response.status < 400) {
|
|
377
|
+
throw new Error("".concat(baseUrl).concat(path, " answered with a ").concat(response.status, " redirect to ") +
|
|
378
|
+
"'".concat(response.headers.location || 'unknown', "'. It is not followed: ") +
|
|
379
|
+
"the sign-in request carries the password. Point the target at the " +
|
|
380
|
+
"final URL.");
|
|
381
|
+
}
|
|
382
|
+
collectCookies(response.headers['set-cookie']);
|
|
383
|
+
return [2 /*return*/, response];
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}); };
|
|
387
|
+
fail = function (message) {
|
|
388
|
+
return new Error("".concat(message, "\nCheck that '").concat(user, "' can sign in to ").concat(target.serverUrl, ", and that ") +
|
|
389
|
+
"the client id is set (SASJS_CLIENT) if the account is not an administrator.");
|
|
390
|
+
};
|
|
391
|
+
// GET / serves the single page app but sets the XSRF cookie the login needs.
|
|
392
|
+
return [4 /*yield*/, call('get', '/')];
|
|
393
|
+
case 1:
|
|
394
|
+
// GET / serves the single page app but sets the XSRF cookie the login needs.
|
|
395
|
+
_k.sent();
|
|
396
|
+
return [4 /*yield*/, call('post', '/SASLogon/login', {
|
|
397
|
+
username: user,
|
|
398
|
+
password: pass
|
|
399
|
+
})];
|
|
400
|
+
case 2:
|
|
401
|
+
login = _k.sent();
|
|
402
|
+
if (login.status !== 200 || !((_a = login.data) === null || _a === void 0 ? void 0 : _a.loggedIn)) {
|
|
403
|
+
throw fail("Login failed for user '".concat(user, "' on ").concat(target.serverUrl, " (HTTP ").concat(login.status, ")."));
|
|
404
|
+
}
|
|
405
|
+
client = clientId || ((_b = target.authConfig) === null || _b === void 0 ? void 0 : _b.client) || '';
|
|
406
|
+
if (!!client) return [3 /*break*/, 4];
|
|
407
|
+
return [4 /*yield*/, call('get', '/SASjsApi/client')];
|
|
408
|
+
case 3:
|
|
409
|
+
clients = _k.sent();
|
|
410
|
+
if (Array.isArray(clients.data) && clients.data.length) {
|
|
411
|
+
client = clients.data[0].clientId;
|
|
412
|
+
// Say which one was picked: the listing order is the server's, and a
|
|
413
|
+
// silent choice here is impossible to debug from the tokens alone.
|
|
414
|
+
(_c = process.logger) === null || _c === void 0 ? void 0 : _c.info("Using the first registered client ('".concat(client, "'). Set SASJS_CLIENT ") +
|
|
415
|
+
"to authorize a specific client.");
|
|
416
|
+
}
|
|
417
|
+
_k.label = 4;
|
|
418
|
+
case 4:
|
|
419
|
+
if (!client) {
|
|
420
|
+
throw fail('No client id is available to authorize.');
|
|
421
|
+
}
|
|
422
|
+
return [4 /*yield*/, call('post', '/SASLogon/authorize', {
|
|
423
|
+
clientId: client
|
|
424
|
+
})];
|
|
425
|
+
case 5:
|
|
426
|
+
authorized = _k.sent();
|
|
427
|
+
code = (_d = authorized.data) === null || _d === void 0 ? void 0 : _d.code;
|
|
428
|
+
if (!code) {
|
|
429
|
+
throw fail("Authorization failed for client '".concat(client, "' (HTTP ").concat(authorized.status, ")."));
|
|
430
|
+
}
|
|
431
|
+
return [4 /*yield*/, call('post', '/SASjsApi/auth/token', {
|
|
432
|
+
clientId: client,
|
|
433
|
+
code: code
|
|
434
|
+
})];
|
|
435
|
+
case 6:
|
|
436
|
+
tokens = _k.sent();
|
|
437
|
+
if (!((_e = tokens.data) === null || _e === void 0 ? void 0 : _e.accessToken)) {
|
|
438
|
+
throw fail("The token endpoint returned no access token (HTTP ".concat(tokens.status, ")."));
|
|
439
|
+
}
|
|
440
|
+
return [2 /*return*/, {
|
|
441
|
+
access_token: tokens.data.accessToken,
|
|
442
|
+
refresh_token: tokens.data.refreshToken || '',
|
|
443
|
+
client: client,
|
|
444
|
+
displayName: ((_g = (_f = login.data) === null || _f === void 0 ? void 0 : _f.user) === null || _g === void 0 ? void 0 : _g.displayName) || ((_j = (_h = login.data) === null || _h === void 0 ? void 0 : _h.user) === null || _j === void 0 ? void 0 : _j.username)
|
|
445
|
+
}];
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
}
|
package/build/utils/config.js
CHANGED
|
@@ -968,7 +968,7 @@ var saveTokens = function (targetName, access_token, refresh_token, client, secr
|
|
|
968
968
|
if (!isLocalTarget) return [3 /*break*/, 3];
|
|
969
969
|
VERBOSE = process.env.VERBOSE;
|
|
970
970
|
clientSecretContent = client
|
|
971
|
-
? "CLIENT=".concat(client, "\
|
|
971
|
+
? "CLIENT=".concat(client, "\n").concat(secret ? "SECRET=".concat(secret, "\n") : '')
|
|
972
972
|
: '';
|
|
973
973
|
envFileContent = "".concat(clientSecretContent, "ACCESS_TOKEN=").concat(access_token, "\nREFRESH_TOKEN=").concat(refresh_token, "\n").concat(VERBOSE ? 'VERBOSE=' + VERBOSE + '\n' : '');
|
|
974
974
|
envFilePath = path_1.default.join(process.projectDir, ".env.".concat(targetName));
|
|
@@ -986,7 +986,7 @@ var saveTokens = function (targetName, access_token, refresh_token, client, secr
|
|
|
986
986
|
}
|
|
987
987
|
targetJson = __assign({}, target);
|
|
988
988
|
targetJson.authConfig = __assign(__assign(__assign({}, (targetJson.authConfig || {})), (client
|
|
989
|
-
? { client: client, secret: secret ||
|
|
989
|
+
? { client: client, secret: secret || undefined }
|
|
990
990
|
: { client: undefined, secret: undefined })), { access_token: access_token, refresh_token: refresh_token });
|
|
991
991
|
return [4 /*yield*/, saveToGlobalConfig(new utils_1.Target(targetJson), globalConfig.defaultTarget === targetName)];
|
|
992
992
|
case 5:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
24
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
50
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
51
|
+
if (!m) return o;
|
|
52
|
+
var i = m.call(o), r, ar = [], e;
|
|
53
|
+
try {
|
|
54
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
55
|
+
}
|
|
56
|
+
catch (error) { e = { error: error }; }
|
|
57
|
+
finally {
|
|
58
|
+
try {
|
|
59
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
60
|
+
}
|
|
61
|
+
finally { if (e) throw e.error; }
|
|
62
|
+
}
|
|
63
|
+
return ar;
|
|
64
|
+
};
|
|
65
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
66
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
67
|
+
if (ar || !(i in from)) {
|
|
68
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
69
|
+
ar[i] = from[i];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
73
|
+
};
|
|
74
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
75
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
76
|
+
};
|
|
77
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
78
|
+
var axios_1 = __importDefault(require("axios"));
|
|
79
|
+
var utils_1 = require("@sasjs/utils");
|
|
80
|
+
var auth_1 = require("../auth");
|
|
81
|
+
jest.mock('axios');
|
|
82
|
+
var request = jest.fn();
|
|
83
|
+
var target = function (client) {
|
|
84
|
+
return new utils_1.Target(__assign({ name: 'sasjs-test', serverType: utils_1.ServerType.Sasjs, serverUrl: 'https://sasjs.example.com/', appLoc: '/Public/app' }, (client ? { authConfig: { client: client } } : {})));
|
|
85
|
+
};
|
|
86
|
+
var ok = function (data, headers) {
|
|
87
|
+
if (headers === void 0) { headers = {}; }
|
|
88
|
+
return ({
|
|
89
|
+
status: 200,
|
|
90
|
+
headers: headers,
|
|
91
|
+
data: data
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
var xsrfCookie = {
|
|
95
|
+
'set-cookie': ['XSRF-TOKEN=abc%3D%3D; Path=/; HttpOnly']
|
|
96
|
+
};
|
|
97
|
+
var happyPath = function (overrides) {
|
|
98
|
+
if (overrides === void 0) { overrides = []; }
|
|
99
|
+
var responses = __spreadArray([
|
|
100
|
+
ok('', xsrfCookie),
|
|
101
|
+
ok({ loggedIn: true, user: { displayName: 'Test User' } }),
|
|
102
|
+
ok([{ clientId: 'client-1' }]),
|
|
103
|
+
ok({ code: 'the-code' }),
|
|
104
|
+
ok({ accessToken: 'access-token', refreshToken: 'refresh-token' })
|
|
105
|
+
], __read(overrides), false);
|
|
106
|
+
responses.forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
107
|
+
};
|
|
108
|
+
describe('getTokensWithSasjsLogin', function () {
|
|
109
|
+
beforeEach(function () {
|
|
110
|
+
jest.clearAllMocks();
|
|
111
|
+
axios_1.default.create.mockReturnValue({ request: request });
|
|
112
|
+
});
|
|
113
|
+
it('should mint a token pair from the four-step local login', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
114
|
+
var result, calls;
|
|
115
|
+
return __generator(this, function (_a) {
|
|
116
|
+
switch (_a.label) {
|
|
117
|
+
case 0:
|
|
118
|
+
happyPath();
|
|
119
|
+
return [4 /*yield*/, (0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')];
|
|
120
|
+
case 1:
|
|
121
|
+
result = _a.sent();
|
|
122
|
+
expect(result).toEqual({
|
|
123
|
+
access_token: 'access-token',
|
|
124
|
+
refresh_token: 'refresh-token',
|
|
125
|
+
client: 'client-1',
|
|
126
|
+
displayName: 'Test User'
|
|
127
|
+
});
|
|
128
|
+
calls = request.mock.calls.map(function (call) { return call[0]; });
|
|
129
|
+
expect(calls.map(function (c) { return c.url; })).toEqual([
|
|
130
|
+
'/',
|
|
131
|
+
'/SASLogon/login',
|
|
132
|
+
'/SASjsApi/client',
|
|
133
|
+
'/SASLogon/authorize',
|
|
134
|
+
'/SASjsApi/auth/token'
|
|
135
|
+
]);
|
|
136
|
+
expect(calls[1].data).toEqual({ username: 'user1', password: 'pass1' });
|
|
137
|
+
expect(calls[3].data).toEqual({ clientId: 'client-1' });
|
|
138
|
+
expect(calls[4].data).toEqual({ clientId: 'client-1', code: 'the-code' });
|
|
139
|
+
return [2 /*return*/];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}); });
|
|
143
|
+
it('should send the session cookie and the decoded XSRF token', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
144
|
+
var loginCall;
|
|
145
|
+
return __generator(this, function (_a) {
|
|
146
|
+
switch (_a.label) {
|
|
147
|
+
case 0:
|
|
148
|
+
happyPath();
|
|
149
|
+
return [4 /*yield*/, (0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')];
|
|
150
|
+
case 1:
|
|
151
|
+
_a.sent();
|
|
152
|
+
loginCall = request.mock.calls[1][0];
|
|
153
|
+
expect(loginCall.headers.Cookie).toBe('XSRF-TOKEN=abc%3D%3D');
|
|
154
|
+
expect(loginCall.headers['X-XSRF-TOKEN']).toBe('abc==');
|
|
155
|
+
// The first call has nothing to send yet.
|
|
156
|
+
expect(request.mock.calls[0][0].headers.Cookie).toBeUndefined();
|
|
157
|
+
return [2 /*return*/];
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}); });
|
|
161
|
+
it('should not list clients when an id is supplied', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
162
|
+
var result;
|
|
163
|
+
return __generator(this, function (_a) {
|
|
164
|
+
switch (_a.label) {
|
|
165
|
+
case 0:
|
|
166
|
+
;
|
|
167
|
+
[
|
|
168
|
+
ok('', xsrfCookie),
|
|
169
|
+
ok({ loggedIn: true }),
|
|
170
|
+
ok({ code: 'the-code' }),
|
|
171
|
+
ok({ accessToken: 'access-token' })
|
|
172
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
173
|
+
return [4 /*yield*/, (0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1', 'pinned-client')];
|
|
174
|
+
case 1:
|
|
175
|
+
result = _a.sent();
|
|
176
|
+
expect(request).toHaveBeenCalledTimes(4);
|
|
177
|
+
expect(result.client).toBe('pinned-client');
|
|
178
|
+
expect(result.refresh_token).toBe('');
|
|
179
|
+
expect(result.displayName).toBeUndefined();
|
|
180
|
+
return [2 /*return*/];
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}); });
|
|
184
|
+
it('should prefer the target client over the admin client listing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
185
|
+
var result;
|
|
186
|
+
return __generator(this, function (_a) {
|
|
187
|
+
switch (_a.label) {
|
|
188
|
+
case 0:
|
|
189
|
+
;
|
|
190
|
+
[
|
|
191
|
+
ok('', xsrfCookie),
|
|
192
|
+
ok({ loggedIn: true }),
|
|
193
|
+
ok({ code: 'the-code' }),
|
|
194
|
+
ok({ accessToken: 'access-token' })
|
|
195
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
196
|
+
return [4 /*yield*/, (0, auth_1.getTokensWithSasjsLogin)(target('from-config'), 'user1', 'pass1')];
|
|
197
|
+
case 1:
|
|
198
|
+
result = _a.sent();
|
|
199
|
+
expect(result.client).toBe('from-config');
|
|
200
|
+
expect(request).toHaveBeenCalledTimes(4);
|
|
201
|
+
return [2 /*return*/];
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}); });
|
|
205
|
+
it('should fail when the credentials are rejected', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
206
|
+
return __generator(this, function (_a) {
|
|
207
|
+
switch (_a.label) {
|
|
208
|
+
case 0:
|
|
209
|
+
;
|
|
210
|
+
[
|
|
211
|
+
ok('', xsrfCookie),
|
|
212
|
+
ok({ loggedIn: false, error: 'invalid credentials' })
|
|
213
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
214
|
+
return [4 /*yield*/, expect((0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'wrong')).rejects.toThrow("Login failed for user 'user1' on https://sasjs.example.com/")];
|
|
215
|
+
case 1:
|
|
216
|
+
_a.sent();
|
|
217
|
+
return [2 /*return*/];
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}); });
|
|
221
|
+
it('should fail, naming SASJS_CLIENT, when no client is available', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
222
|
+
return __generator(this, function (_a) {
|
|
223
|
+
switch (_a.label) {
|
|
224
|
+
case 0:
|
|
225
|
+
;
|
|
226
|
+
[
|
|
227
|
+
ok('', xsrfCookie),
|
|
228
|
+
ok({ loggedIn: true }),
|
|
229
|
+
{ status: 403, headers: {}, data: 'Forbidden' }
|
|
230
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
231
|
+
return [4 /*yield*/, expect((0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')).rejects.toThrow(/No client id is available to authorize/)];
|
|
232
|
+
case 1:
|
|
233
|
+
_a.sent();
|
|
234
|
+
return [2 /*return*/];
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}); });
|
|
238
|
+
it('should fail when the token endpoint returns nothing usable', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
239
|
+
return __generator(this, function (_a) {
|
|
240
|
+
switch (_a.label) {
|
|
241
|
+
case 0:
|
|
242
|
+
;
|
|
243
|
+
[
|
|
244
|
+
ok('', xsrfCookie),
|
|
245
|
+
ok({ loggedIn: true }),
|
|
246
|
+
ok([{ clientId: 'client-1' }]),
|
|
247
|
+
ok({ code: 'the-code' }),
|
|
248
|
+
ok({})
|
|
249
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
250
|
+
return [4 /*yield*/, expect((0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')).rejects.toThrow(/returned no access token/)];
|
|
251
|
+
case 1:
|
|
252
|
+
_a.sent();
|
|
253
|
+
return [2 /*return*/];
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}); });
|
|
257
|
+
it('should refuse to follow a redirect that would replay the password', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
258
|
+
return __generator(this, function (_a) {
|
|
259
|
+
switch (_a.label) {
|
|
260
|
+
case 0:
|
|
261
|
+
;
|
|
262
|
+
[
|
|
263
|
+
ok('', xsrfCookie),
|
|
264
|
+
{
|
|
265
|
+
status: 307,
|
|
266
|
+
headers: { location: 'https://elsewhere.example.com/login' },
|
|
267
|
+
data: {}
|
|
268
|
+
}
|
|
269
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
270
|
+
return [4 /*yield*/, expect((0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')).rejects.toThrow(/answered with a 307 redirect/)];
|
|
271
|
+
case 1:
|
|
272
|
+
_a.sent();
|
|
273
|
+
return [2 /*return*/];
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}); });
|
|
277
|
+
it('should fall back to the raw XSRF value when it is not valid encoding', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
278
|
+
return __generator(this, function (_a) {
|
|
279
|
+
switch (_a.label) {
|
|
280
|
+
case 0:
|
|
281
|
+
;
|
|
282
|
+
[
|
|
283
|
+
ok('', { 'set-cookie': ['XSRF-TOKEN=abc%ZZ; Path=/'] }),
|
|
284
|
+
ok({ loggedIn: true }),
|
|
285
|
+
ok([{ clientId: 'client-1' }]),
|
|
286
|
+
ok({ code: 'the-code' }),
|
|
287
|
+
ok({ accessToken: 'access-token' })
|
|
288
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
289
|
+
return [4 /*yield*/, (0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')];
|
|
290
|
+
case 1:
|
|
291
|
+
_a.sent();
|
|
292
|
+
expect(request.mock.calls[1][0].headers['X-XSRF-TOKEN']).toBe('abc%ZZ');
|
|
293
|
+
return [2 /*return*/];
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}); });
|
|
297
|
+
it('should hand the target TLS options to the http client', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
298
|
+
var options;
|
|
299
|
+
return __generator(this, function (_a) {
|
|
300
|
+
switch (_a.label) {
|
|
301
|
+
case 0:
|
|
302
|
+
happyPath();
|
|
303
|
+
return [4 /*yield*/, (0, auth_1.getTokensWithSasjsLogin)(new utils_1.Target({
|
|
304
|
+
name: 'sasjs-tls',
|
|
305
|
+
serverType: utils_1.ServerType.Sasjs,
|
|
306
|
+
serverUrl: 'https://sasjs.example.com',
|
|
307
|
+
appLoc: '/Public/app',
|
|
308
|
+
httpsAgentOptions: { rejectUnauthorized: false }
|
|
309
|
+
}), 'user1', 'pass1')];
|
|
310
|
+
case 1:
|
|
311
|
+
_a.sent();
|
|
312
|
+
options = axios_1.default.create.mock.calls[0][0];
|
|
313
|
+
expect(options.httpsAgent).toBeDefined();
|
|
314
|
+
expect(options.maxRedirects).toBe(0);
|
|
315
|
+
// Every status is inspected rather than thrown by axios, so the flow can
|
|
316
|
+
// report a 401 or a 307 in its own words.
|
|
317
|
+
expect(options.validateStatus(500)).toBe(true);
|
|
318
|
+
return [2 /*return*/];
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}); });
|
|
322
|
+
it('should name an unnamed redirect target instead of printing undefined', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
323
|
+
return __generator(this, function (_a) {
|
|
324
|
+
switch (_a.label) {
|
|
325
|
+
case 0:
|
|
326
|
+
;
|
|
327
|
+
[ok('', xsrfCookie), { status: 302, headers: {}, data: {} }].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
328
|
+
return [4 /*yield*/, expect((0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')).rejects.toThrow(/redirect to 'unknown'/)];
|
|
329
|
+
case 1:
|
|
330
|
+
_a.sent();
|
|
331
|
+
return [2 /*return*/];
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}); });
|
|
335
|
+
it('should fail when the authorize step returns no code', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
336
|
+
return __generator(this, function (_a) {
|
|
337
|
+
switch (_a.label) {
|
|
338
|
+
case 0:
|
|
339
|
+
;
|
|
340
|
+
[
|
|
341
|
+
ok('', xsrfCookie),
|
|
342
|
+
ok({ loggedIn: true }),
|
|
343
|
+
ok([{ clientId: 'client-1' }]),
|
|
344
|
+
ok({})
|
|
345
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
346
|
+
return [4 /*yield*/, expect((0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')).rejects.toThrow(/Authorization failed for client 'client-1' \(HTTP 200\)/)];
|
|
347
|
+
case 1:
|
|
348
|
+
_a.sent();
|
|
349
|
+
return [2 /*return*/];
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}); });
|
|
353
|
+
it('should ignore a client listing that is not an array', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
354
|
+
return __generator(this, function (_a) {
|
|
355
|
+
switch (_a.label) {
|
|
356
|
+
case 0:
|
|
357
|
+
;
|
|
358
|
+
[
|
|
359
|
+
ok('', xsrfCookie),
|
|
360
|
+
ok({ loggedIn: true }),
|
|
361
|
+
ok({ message: 'not a list' })
|
|
362
|
+
].forEach(function (response) { return request.mockResolvedValueOnce(response); });
|
|
363
|
+
return [4 /*yield*/, expect((0, auth_1.getTokensWithSasjsLogin)(target(), 'user1', 'pass1')).rejects.toThrow(/No client id is available to authorize/)];
|
|
364
|
+
case 1:
|
|
365
|
+
_a.sent();
|
|
366
|
+
return [2 /*return*/];
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}); });
|
|
370
|
+
});
|
package/package.json
CHANGED