supaapps-auth 1.2.0 → 1.4.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/.github/workflows/ci.yml +31 -0
- package/dist/AuthManager.d.ts +12 -9
- package/dist/AuthManager.js +102 -217
- package/jest.config.js +16 -0
- package/package.json +8 -2
- package/src/AuthManager.ts +114 -222
- package/tests/AuthManager.test.ts +107 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Node.js CI
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
node-version: [18.x, 20.x]
|
|
14
|
+
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v3
|
|
18
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
19
|
+
uses: actions/setup-node@v3
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node-version }}
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
- run: npm ci
|
|
24
|
+
- run: npm run build --if-present
|
|
25
|
+
- run: npm run test
|
|
26
|
+
|
|
27
|
+
- name: Upload coverage reports to Codecov
|
|
28
|
+
uses: codecov/codecov-action@v4.0.1
|
|
29
|
+
with:
|
|
30
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
31
|
+
slug: supaapps/supaapps-auth
|
package/dist/AuthManager.d.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
export declare class AuthManager {
|
|
2
2
|
private static instance;
|
|
3
|
-
private
|
|
4
|
-
private
|
|
5
|
-
private
|
|
6
|
-
private
|
|
7
|
-
constructor(
|
|
8
|
-
static
|
|
3
|
+
private authServer;
|
|
4
|
+
private realmName;
|
|
5
|
+
private redirectUri;
|
|
6
|
+
private loginCallback;
|
|
7
|
+
private constructor();
|
|
8
|
+
static initialize(authServer: string, realmName: string, redirectUri: string, loginCallback: () => void): AuthManager;
|
|
9
|
+
static getInstance(): AuthManager;
|
|
9
10
|
private toBase64Url;
|
|
10
11
|
private generatePKCEPair;
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
refreshAccessToken(): Promise<string>;
|
|
13
|
+
checkAccessToken(): Promise<string>;
|
|
14
|
+
private isTokenExpired;
|
|
13
15
|
mustBeLoggedIn(): Promise<boolean>;
|
|
14
16
|
getLoginWithGoogleUri(): string;
|
|
15
17
|
isLoggedIn(): Promise<boolean>;
|
|
16
18
|
getAccessToken(): Promise<string>;
|
|
17
|
-
loginUsingPkce(code:
|
|
19
|
+
loginUsingPkce(code: string): Promise<void>;
|
|
18
20
|
logout(): Promise<void>;
|
|
19
21
|
static validateToken(authServer: string, bearerToken: string): Promise<boolean>;
|
|
22
|
+
static resetInstance(): void;
|
|
20
23
|
}
|
package/dist/AuthManager.js
CHANGED
|
@@ -12,275 +12,160 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.AuthManager = void 0;
|
|
13
13
|
const axios_1 = require("axios");
|
|
14
14
|
const crypto_1 = require("crypto");
|
|
15
|
+
const jsonwebtoken_1 = require("jsonwebtoken"); // Ensure jsonwebtoken is correctly imported
|
|
15
16
|
class AuthManager {
|
|
16
17
|
constructor(authServer, realmName, redirectUri, loginCallback) {
|
|
17
|
-
this.authServer = null;
|
|
18
|
-
this.realmName = null;
|
|
19
|
-
this.redirectUri = null;
|
|
20
|
-
this.loginCallback = () => { };
|
|
21
|
-
this.toBase64Url = (base64String) => {
|
|
22
|
-
return base64String
|
|
23
|
-
.replace(/\+/g, '-')
|
|
24
|
-
.replace(/\//g, '_')
|
|
25
|
-
.replace(/=+$/, '');
|
|
26
|
-
};
|
|
27
|
-
this.generatePKCEPair = () => {
|
|
28
|
-
const NUM_OF_BYTES = 32; // This will generate a verifier of sufficient length
|
|
29
|
-
const HASH_ALG = 'sha256';
|
|
30
|
-
// Generate code verifier
|
|
31
|
-
const newCodeVerifier = this.toBase64Url((0, crypto_1.randomBytes)(NUM_OF_BYTES).toString('base64'));
|
|
32
|
-
// Generate code challenge
|
|
33
|
-
const hash = (0, crypto_1.createHash)(HASH_ALG)
|
|
34
|
-
.update(newCodeVerifier)
|
|
35
|
-
.digest('base64');
|
|
36
|
-
const newCodeChallenge = this.toBase64Url(hash);
|
|
37
|
-
return { newCodeVerifier, newCodeChallenge };
|
|
38
|
-
};
|
|
39
18
|
this.authServer = authServer;
|
|
40
19
|
this.realmName = realmName;
|
|
41
20
|
this.redirectUri = redirectUri;
|
|
42
21
|
this.loginCallback = loginCallback;
|
|
43
22
|
AuthManager.instance = this;
|
|
44
23
|
}
|
|
24
|
+
static initialize(authServer, realmName, redirectUri, loginCallback) {
|
|
25
|
+
if (!AuthManager.instance) {
|
|
26
|
+
AuthManager.instance = new AuthManager(authServer, realmName, redirectUri, loginCallback);
|
|
27
|
+
}
|
|
28
|
+
return AuthManager.instance;
|
|
29
|
+
}
|
|
45
30
|
static getInstance() {
|
|
46
31
|
if (!AuthManager.instance) {
|
|
47
32
|
throw new Error('AuthManager not initialized');
|
|
48
33
|
}
|
|
49
34
|
return AuthManager.instance;
|
|
50
35
|
}
|
|
36
|
+
toBase64Url(base64String) {
|
|
37
|
+
return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
38
|
+
}
|
|
39
|
+
generatePKCEPair() {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const verifier = (_a = localStorage.getItem('codeVerifier')) !== null && _a !== void 0 ? _a : this.toBase64Url((0, crypto_1.randomBytes)(32).toString('base64'));
|
|
42
|
+
const challenge = (_b = localStorage.getItem('codeChallenge')) !== null && _b !== void 0 ? _b : this.toBase64Url((0, crypto_1.createHash)('sha256').update(verifier).digest('base64'));
|
|
43
|
+
localStorage.setItem('codeVerifier', verifier);
|
|
44
|
+
localStorage.setItem('codeChallenge', challenge);
|
|
45
|
+
return { verifier, challenge };
|
|
46
|
+
}
|
|
51
47
|
refreshAccessToken() {
|
|
52
48
|
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new Error('No refresh token found');
|
|
58
|
-
}
|
|
59
|
-
const decodedRefreshToken = JSON.parse(atob(refreshToken.split('.')[1]));
|
|
60
|
-
if (decodedRefreshToken) {
|
|
61
|
-
const currentTime = Date.now() / 1000;
|
|
62
|
-
if (decodedRefreshToken.exp < currentTime) {
|
|
63
|
-
throw new Error('Refresh token expired');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
yield fetch(`${this.authServer}auth/refresh`, {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
},
|
|
71
|
-
body: JSON.stringify({
|
|
72
|
-
refresh_token: refreshToken,
|
|
73
|
-
}),
|
|
74
|
-
})
|
|
75
|
-
.then((response) => {
|
|
76
|
-
if (response.status !== 200) {
|
|
77
|
-
throw new Error('Failed to refresh the token');
|
|
78
|
-
}
|
|
79
|
-
return response.json();
|
|
80
|
-
})
|
|
81
|
-
.then((exchangeJson) => {
|
|
82
|
-
localStorage.setItem('refresh_token', exchangeJson.refresh_token);
|
|
83
|
-
localStorage.setItem('access_token', exchangeJson.access_token);
|
|
84
|
-
resolve(exchangeJson.access_token);
|
|
85
|
-
})
|
|
86
|
-
.catch((error) => {
|
|
87
|
-
reject(error);
|
|
88
|
-
});
|
|
49
|
+
try {
|
|
50
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
51
|
+
if (!refreshToken) {
|
|
52
|
+
throw new Error('No refresh token found');
|
|
89
53
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
54
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/refresh`, {
|
|
55
|
+
refresh_token: refreshToken
|
|
56
|
+
});
|
|
57
|
+
localStorage.setItem('refresh_token', response.data.refresh_token);
|
|
58
|
+
localStorage.setItem('access_token', response.data.access_token);
|
|
59
|
+
return response.data.access_token;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`Refresh token error, logging out: ${error}`);
|
|
63
|
+
localStorage.removeItem('access_token');
|
|
64
|
+
localStorage.removeItem('refresh_token');
|
|
65
|
+
this.loginCallback();
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
94
68
|
});
|
|
95
69
|
}
|
|
96
70
|
checkAccessToken() {
|
|
97
71
|
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
// decode access token and check if it's expired
|
|
105
|
-
const decodedToken = accessToken
|
|
106
|
-
? JSON.parse(atob(accessToken.split('.')[1]))
|
|
107
|
-
: null;
|
|
108
|
-
if (decodedToken) {
|
|
109
|
-
const currentTime = Date.now() / 1000;
|
|
110
|
-
if (decodedToken.exp < currentTime) {
|
|
111
|
-
// refreshing expired token
|
|
112
|
-
const newAccessToken = yield this.refreshAccessToken();
|
|
113
|
-
return resolve(newAccessToken);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
resolve(accessToken);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
reject(error);
|
|
120
|
-
}
|
|
121
|
-
}));
|
|
72
|
+
let accessToken = localStorage.getItem('access_token');
|
|
73
|
+
if (!accessToken || this.isTokenExpired(accessToken)) {
|
|
74
|
+
return this.refreshAccessToken();
|
|
75
|
+
}
|
|
76
|
+
return accessToken;
|
|
122
77
|
});
|
|
123
78
|
}
|
|
79
|
+
isTokenExpired(token) {
|
|
80
|
+
const decoded = JSON.parse(atob(token.split('.')[1]));
|
|
81
|
+
return decoded.exp < Date.now() / 1000;
|
|
82
|
+
}
|
|
124
83
|
mustBeLoggedIn() {
|
|
125
84
|
return __awaiter(this, void 0, void 0, function* () {
|
|
126
|
-
return
|
|
127
|
-
this.isLoggedIn().then((isLoggedIn) => {
|
|
128
|
-
if (!isLoggedIn) {
|
|
129
|
-
this.loginCallback();
|
|
130
|
-
return resolve(false);
|
|
131
|
-
}
|
|
132
|
-
return resolve(true);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
85
|
+
return this.isLoggedIn() || (this.loginCallback(), false);
|
|
135
86
|
});
|
|
136
87
|
}
|
|
137
88
|
getLoginWithGoogleUri() {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let codeVerifier = localStorage.getItem('codeVerifier') || newCodeVerifier;
|
|
141
|
-
let codeChallenge = localStorage.getItem('codeChallenge') || newCodeChallenge;
|
|
142
|
-
localStorage.setItem('codeVerifier', codeVerifier);
|
|
143
|
-
localStorage.setItem('codeChallenge', codeChallenge);
|
|
144
|
-
if (this.authServer && this.realmName && this.redirectUri) {
|
|
145
|
-
return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}` +
|
|
146
|
-
`&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
|
147
|
-
}
|
|
89
|
+
const { challenge } = this.generatePKCEPair();
|
|
90
|
+
return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256`;
|
|
148
91
|
}
|
|
149
92
|
isLoggedIn() {
|
|
150
93
|
return __awaiter(this, void 0, void 0, function* () {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
reject(error);
|
|
159
|
-
}
|
|
160
|
-
}));
|
|
94
|
+
try {
|
|
95
|
+
yield this.checkAccessToken();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
161
101
|
});
|
|
162
102
|
}
|
|
163
103
|
getAccessToken() {
|
|
164
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
165
|
-
|
|
166
|
-
// otherwise throw error
|
|
167
|
-
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
168
|
-
try {
|
|
169
|
-
const accessToken = yield this.checkAccessToken();
|
|
170
|
-
return resolve(accessToken);
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
reject(error);
|
|
174
|
-
}
|
|
175
|
-
}));
|
|
105
|
+
return this.checkAccessToken();
|
|
176
106
|
});
|
|
177
107
|
}
|
|
178
108
|
loginUsingPkce(code) {
|
|
179
109
|
return __awaiter(this, void 0, void 0, function* () {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fetch(`${this.authServer}auth/pkce_exchange`, {
|
|
185
|
-
method: 'POST',
|
|
186
|
-
headers: {
|
|
187
|
-
'Content-Type': 'application/json',
|
|
188
|
-
},
|
|
189
|
-
body: JSON.stringify({
|
|
190
|
-
realm_name: this.realmName,
|
|
191
|
-
code: code,
|
|
192
|
-
redirect_uri: this.redirectUri,
|
|
193
|
-
code_verifier: codeVerifier,
|
|
194
|
-
}),
|
|
195
|
-
})
|
|
196
|
-
.then((response) => {
|
|
197
|
-
localStorage.removeItem('codeVerifier');
|
|
198
|
-
localStorage.removeItem('codeChallenge');
|
|
199
|
-
if (response.status !== 200) {
|
|
200
|
-
throw new Error('Failed to exchange code for token');
|
|
201
|
-
}
|
|
202
|
-
return response.json();
|
|
203
|
-
})
|
|
204
|
-
.then((exchangeJson) => {
|
|
205
|
-
localStorage.setItem('access_token', exchangeJson.access_token);
|
|
206
|
-
localStorage.setItem('refresh_token', exchangeJson.refresh_token);
|
|
207
|
-
resolve();
|
|
208
|
-
})
|
|
209
|
-
.catch((error) => {
|
|
210
|
-
localStorage.removeItem('codeVerifier');
|
|
211
|
-
localStorage.removeItem('codeChallenge');
|
|
212
|
-
reject(error);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
reject(error);
|
|
110
|
+
try {
|
|
111
|
+
const codeVerifier = localStorage.getItem('codeVerifier');
|
|
112
|
+
if (!codeVerifier) {
|
|
113
|
+
throw new Error('Code verifier not found');
|
|
218
114
|
}
|
|
219
|
-
|
|
115
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/pkce_exchange`, {
|
|
116
|
+
realm_name: this.realmName,
|
|
117
|
+
code,
|
|
118
|
+
redirect_uri: this.redirectUri,
|
|
119
|
+
code_verifier: codeVerifier,
|
|
120
|
+
});
|
|
121
|
+
localStorage.setItem('access_token', response.data.access_token);
|
|
122
|
+
localStorage.setItem('refresh_token', response.data.refresh_token);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
localStorage.removeItem('codeVerifier');
|
|
126
|
+
localStorage.removeItem('codeChallenge');
|
|
127
|
+
}
|
|
220
128
|
});
|
|
221
129
|
}
|
|
222
130
|
logout() {
|
|
223
131
|
return __awaiter(this, void 0, void 0, function* () {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
localStorage.removeItem('refresh_token');
|
|
229
|
-
fetch(`${this.authServer}auth/logout`, {
|
|
230
|
-
method: 'POST',
|
|
231
|
-
headers: {
|
|
232
|
-
'Content-Type': 'application/json',
|
|
233
|
-
'Authorization': `Bearer ${bearerToken}`,
|
|
234
|
-
},
|
|
235
|
-
}).then((response) => {
|
|
236
|
-
if (response.status !== 200) {
|
|
237
|
-
throw new Error('Failed to attempt logout');
|
|
238
|
-
}
|
|
239
|
-
resolve();
|
|
240
|
-
}).catch((error) => {
|
|
241
|
-
reject(error);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
reject(error);
|
|
132
|
+
try {
|
|
133
|
+
const accessToken = localStorage.getItem('access_token');
|
|
134
|
+
if (!accessToken) {
|
|
135
|
+
throw new Error('Access token not found');
|
|
246
136
|
}
|
|
247
|
-
|
|
137
|
+
yield axios_1.default.post(`${this.authServer}auth/logout`, {}, {
|
|
138
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
localStorage.removeItem('access_token');
|
|
143
|
+
localStorage.removeItem('refresh_token');
|
|
144
|
+
}
|
|
248
145
|
});
|
|
249
146
|
}
|
|
250
147
|
static validateToken(authServer, bearerToken) {
|
|
148
|
+
var _a;
|
|
251
149
|
return __awaiter(this, void 0, void 0, function* () {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (!decodedToken) {
|
|
257
|
-
return resolve(false);
|
|
258
|
-
}
|
|
259
|
-
const currentTime = Date.now() / 1000;
|
|
260
|
-
if (decodedToken.exp < currentTime) {
|
|
261
|
-
return resolve(false);
|
|
262
|
-
}
|
|
263
|
-
const { data: publicKey } = yield axios_1.default.get(`${authServer}public/public_key`);
|
|
264
|
-
const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
|
|
265
|
-
const jwt = require('jsonwebtoken');
|
|
266
|
-
jwt.verify(accessToken, publicKey, { algorithms: [algo] }, (error, payload) => {
|
|
267
|
-
if (error) {
|
|
268
|
-
return resolve(false);
|
|
269
|
-
}
|
|
270
|
-
axios_1.default.get(`${authServer}public/revoked_ids`).then(({ data: revokedIds }) => {
|
|
271
|
-
if (revokedIds && revokedIds.includes(decodedToken['id'])) {
|
|
272
|
-
return resolve(false);
|
|
273
|
-
}
|
|
274
|
-
return resolve(true);
|
|
275
|
-
});
|
|
276
|
-
});
|
|
150
|
+
try {
|
|
151
|
+
const decodedToken = (_a = jsonwebtoken_1.default.decode(bearerToken, { complete: true })) === null || _a === void 0 ? void 0 : _a.payload;
|
|
152
|
+
if (!decodedToken || decodedToken.exp < Date.now() / 1000) {
|
|
153
|
+
return false;
|
|
277
154
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
155
|
+
const { data: publicKey } = yield axios_1.default.get(`${authServer}public/public_key`);
|
|
156
|
+
const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
|
|
157
|
+
jsonwebtoken_1.default.verify(bearerToken, publicKey, { algorithms: [algo] });
|
|
158
|
+
const { data: revokedIds } = yield axios_1.default.get(`${authServer}public/revoked_ids`);
|
|
159
|
+
return !revokedIds.includes(decodedToken['id']);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
282
164
|
});
|
|
283
165
|
}
|
|
166
|
+
static resetInstance() {
|
|
167
|
+
AuthManager.instance = null;
|
|
168
|
+
}
|
|
284
169
|
}
|
|
285
170
|
exports.AuthManager = AuthManager;
|
|
286
171
|
AuthManager.instance = null;
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'jsdom', // Use jsdom environment to run tests
|
|
3
|
+
roots: ['<rootDir>/tests'],
|
|
4
|
+
testMatch: ['**/*.test.ts'], // Matches test files in the tests directory
|
|
5
|
+
transform: {
|
|
6
|
+
'^.+\\.tsx?$': 'ts-jest', // Transform TypeScript files using ts-jest
|
|
7
|
+
},
|
|
8
|
+
setupFilesAfterEnv: ['jest-localstorage-mock'], // Setup local storage mock for all tests
|
|
9
|
+
testEnvironment: 'node', // Use node environment to run tests,
|
|
10
|
+
collectCoverage: true,
|
|
11
|
+
coverageReporters: [
|
|
12
|
+
"text",
|
|
13
|
+
"cobertura"
|
|
14
|
+
]
|
|
15
|
+
};
|
|
16
|
+
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supaapps-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "jest",
|
|
9
9
|
"build": "tsc"
|
|
10
10
|
},
|
|
11
11
|
"author": "",
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
"jsonwebtoken": "^9.0.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
+
"@types/jest": "^29.5.12",
|
|
19
20
|
"@types/node": "^20.11.10",
|
|
21
|
+
"axios-mock-adapter": "^1.22.0",
|
|
22
|
+
"jest": "^29.7.0",
|
|
23
|
+
"jest-localstorage-mock": "^2.4.26",
|
|
24
|
+
"jest-mock-axios": "^4.7.3",
|
|
25
|
+
"ts-jest": "^29.1.2",
|
|
20
26
|
"typescript": "^5.3.3"
|
|
21
27
|
}
|
|
22
28
|
}
|
package/src/AuthManager.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { createHash, randomBytes } from 'crypto';
|
|
3
|
+
import jwt from 'jsonwebtoken'; // Ensure jsonwebtoken is correctly imported
|
|
3
4
|
|
|
4
5
|
export class AuthManager {
|
|
5
6
|
private static instance: AuthManager | null = null;
|
|
6
|
-
private
|
|
7
|
+
private authServer: string;
|
|
8
|
+
private realmName: string;
|
|
9
|
+
private redirectUri: string;
|
|
10
|
+
private loginCallback: () => void;
|
|
7
11
|
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
private readonly redirectUri: string | null = null;
|
|
11
|
-
private readonly loginCallback: () => void = () => {};
|
|
12
|
-
|
|
13
|
-
public constructor(authServer: string, realmName: string, redirectUri: string, loginCallback: () => void) {
|
|
12
|
+
private constructor(authServer: string, realmName: string, redirectUri: string, loginCallback: () => void) {
|
|
14
13
|
this.authServer = authServer;
|
|
15
14
|
this.realmName = realmName;
|
|
16
15
|
this.redirectUri = redirectUri;
|
|
@@ -18,257 +17,150 @@ export class AuthManager {
|
|
|
18
17
|
AuthManager.instance = this;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
public static
|
|
20
|
+
public static initialize(authServer: string, realmName: string, redirectUri: string, loginCallback: () => void): AuthManager {
|
|
21
|
+
if (!AuthManager.instance) {
|
|
22
|
+
AuthManager.instance = new AuthManager(authServer, realmName, redirectUri, loginCallback);
|
|
23
|
+
}
|
|
24
|
+
return AuthManager.instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static getInstance(): AuthManager {
|
|
22
28
|
if (!AuthManager.instance) {
|
|
23
29
|
throw new Error('AuthManager not initialized');
|
|
24
30
|
}
|
|
25
31
|
return AuthManager.instance;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
private
|
|
29
|
-
return base64String
|
|
30
|
-
|
|
31
|
-
.replace(/\//g, '_')
|
|
32
|
-
.replace(/=+$/, '');
|
|
33
|
-
};
|
|
34
|
-
private generatePKCEPair = () => {
|
|
35
|
-
const NUM_OF_BYTES = 32; // This will generate a verifier of sufficient length
|
|
36
|
-
const HASH_ALG = 'sha256';
|
|
34
|
+
private toBase64Url(base64String: string): string {
|
|
35
|
+
return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
36
|
+
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
);
|
|
38
|
+
private generatePKCEPair(): { verifier: string; challenge: string } {
|
|
39
|
+
const verifier = localStorage.getItem('codeVerifier') ?? this.toBase64Url(randomBytes(32).toString('base64'));
|
|
40
|
+
const challenge = localStorage.getItem('codeChallenge') ?? this.toBase64Url(createHash('sha256').update(verifier).digest('base64'));
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.update(newCodeVerifier)
|
|
46
|
-
.digest('base64');
|
|
47
|
-
const newCodeChallenge = this.toBase64Url(hash);
|
|
42
|
+
localStorage.setItem('codeVerifier', verifier);
|
|
43
|
+
localStorage.setItem('codeChallenge', challenge);
|
|
48
44
|
|
|
49
|
-
return {
|
|
50
|
-
}
|
|
45
|
+
return { verifier, challenge };
|
|
46
|
+
}
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new Error('No refresh token found');
|
|
58
|
-
}
|
|
59
|
-
const decodedRefreshToken = JSON.parse(atob(refreshToken.split('.')[1]));
|
|
60
|
-
if (decodedRefreshToken) {
|
|
61
|
-
const currentTime = Date.now() / 1000;
|
|
62
|
-
if (decodedRefreshToken.exp < currentTime) {
|
|
63
|
-
throw new Error('Refresh token expired');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
await fetch(`${this.authServer}auth/refresh`, {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
},
|
|
71
|
-
body: JSON.stringify({
|
|
72
|
-
refresh_token: refreshToken,
|
|
73
|
-
}),
|
|
74
|
-
})
|
|
75
|
-
.then((response) => {
|
|
76
|
-
if (response.status !== 200) {
|
|
77
|
-
throw new Error('Failed to refresh the token');
|
|
78
|
-
}
|
|
79
|
-
return response.json();
|
|
80
|
-
})
|
|
81
|
-
.then((exchangeJson) => {
|
|
82
|
-
localStorage.setItem('refresh_token', exchangeJson.refresh_token);
|
|
83
|
-
localStorage.setItem('access_token', exchangeJson.access_token);
|
|
84
|
-
resolve(exchangeJson.access_token);
|
|
85
|
-
})
|
|
86
|
-
.catch((error) => {
|
|
87
|
-
reject(error);
|
|
88
|
-
});
|
|
89
|
-
} catch (error) {
|
|
90
|
-
reject(error);
|
|
48
|
+
public async refreshAccessToken(): Promise<string> {
|
|
49
|
+
try {
|
|
50
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
51
|
+
if (!refreshToken) {
|
|
52
|
+
throw new Error('No refresh token found');
|
|
91
53
|
}
|
|
92
|
-
|
|
54
|
+
|
|
55
|
+
const response = await axios.post(`${this.authServer}auth/refresh`, {
|
|
56
|
+
refresh_token: refreshToken
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
localStorage.setItem('refresh_token', response.data.refresh_token);
|
|
60
|
+
localStorage.setItem('access_token', response.data.access_token);
|
|
61
|
+
return response.data.access_token;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`Refresh token error, logging out: ${error}`);
|
|
64
|
+
localStorage.removeItem('access_token');
|
|
65
|
+
localStorage.removeItem('refresh_token');
|
|
66
|
+
this.loginCallback();
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
93
69
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const currentTime = Date.now() / 1000;
|
|
107
|
-
if (decodedToken.exp < currentTime) {
|
|
108
|
-
// refreshing expired token
|
|
109
|
-
const newAccessToken = await this.refreshAccessToken();
|
|
110
|
-
return resolve(newAccessToken);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
resolve(accessToken);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
reject(error);
|
|
116
|
-
}
|
|
117
|
-
})
|
|
70
|
+
|
|
71
|
+
public async checkAccessToken(): Promise<string> {
|
|
72
|
+
let accessToken = localStorage.getItem('access_token');
|
|
73
|
+
if (!accessToken || this.isTokenExpired(accessToken)) {
|
|
74
|
+
return this.refreshAccessToken();
|
|
75
|
+
}
|
|
76
|
+
return accessToken;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private isTokenExpired(token: string): boolean {
|
|
80
|
+
const decoded = JSON.parse(atob(token.split('.')[1]));
|
|
81
|
+
return decoded.exp < Date.now() / 1000;
|
|
118
82
|
}
|
|
119
83
|
|
|
120
84
|
public async mustBeLoggedIn(): Promise<boolean> {
|
|
121
|
-
return
|
|
122
|
-
this.isLoggedIn().then((isLoggedIn) => {
|
|
123
|
-
if (!isLoggedIn) {
|
|
124
|
-
this.loginCallback();
|
|
125
|
-
return resolve(false);
|
|
126
|
-
}
|
|
127
|
-
return resolve(true);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
85
|
+
return this.isLoggedIn() || (this.loginCallback(), false);
|
|
130
86
|
}
|
|
131
87
|
|
|
132
88
|
public getLoginWithGoogleUri(): string {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
let codeVerifier = localStorage.getItem('codeVerifier') || newCodeVerifier;
|
|
136
|
-
let codeChallenge = localStorage.getItem('codeChallenge') || newCodeChallenge;
|
|
137
|
-
localStorage.setItem('codeVerifier', codeVerifier);
|
|
138
|
-
localStorage.setItem('codeChallenge', codeChallenge);
|
|
139
|
-
|
|
140
|
-
if (this.authServer && this.realmName && this.redirectUri) {
|
|
141
|
-
return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}` +
|
|
142
|
-
`&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256`
|
|
143
|
-
}
|
|
89
|
+
const { challenge } = this.generatePKCEPair();
|
|
90
|
+
return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256`;
|
|
144
91
|
}
|
|
145
92
|
|
|
146
93
|
public async isLoggedIn(): Promise<boolean> {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
reject(error);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
94
|
+
try {
|
|
95
|
+
await this.checkAccessToken();
|
|
96
|
+
return true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
156
100
|
}
|
|
157
101
|
|
|
158
102
|
public async getAccessToken(): Promise<string> {
|
|
159
|
-
|
|
160
|
-
// otherwise throw error
|
|
161
|
-
return new Promise(async (resolve, reject) => {
|
|
162
|
-
try {
|
|
163
|
-
const accessToken = await this.checkAccessToken();
|
|
164
|
-
|
|
165
|
-
return resolve(accessToken);
|
|
166
|
-
} catch (error) {
|
|
167
|
-
reject(error);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
103
|
+
return this.checkAccessToken();
|
|
170
104
|
}
|
|
171
105
|
|
|
172
|
-
public async loginUsingPkce(code): Promise<void> {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
fetch(`${this.authServer}auth/pkce_exchange`, {
|
|
178
|
-
method: 'POST',
|
|
179
|
-
headers: {
|
|
180
|
-
'Content-Type': 'application/json',
|
|
181
|
-
},
|
|
182
|
-
body: JSON.stringify({
|
|
183
|
-
realm_name: this.realmName,
|
|
184
|
-
code: code,
|
|
185
|
-
redirect_uri: this.redirectUri,
|
|
186
|
-
code_verifier: codeVerifier,
|
|
187
|
-
}),
|
|
188
|
-
})
|
|
189
|
-
.then((response) => {
|
|
190
|
-
localStorage.removeItem('codeVerifier');
|
|
191
|
-
localStorage.removeItem('codeChallenge');
|
|
192
|
-
if (response.status !== 200) {
|
|
193
|
-
throw new Error('Failed to exchange code for token');
|
|
194
|
-
}
|
|
195
|
-
return response.json();
|
|
196
|
-
})
|
|
197
|
-
.then((exchangeJson) => {
|
|
198
|
-
localStorage.setItem('access_token', exchangeJson.access_token);
|
|
199
|
-
localStorage.setItem('refresh_token', exchangeJson.refresh_token);
|
|
200
|
-
resolve();
|
|
201
|
-
})
|
|
202
|
-
.catch((error) => {
|
|
203
|
-
localStorage.removeItem('codeVerifier');
|
|
204
|
-
localStorage.removeItem('codeChallenge');
|
|
205
|
-
reject(error);
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
} catch (error) {
|
|
209
|
-
reject(error);
|
|
106
|
+
public async loginUsingPkce(code: string): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
const codeVerifier = localStorage.getItem('codeVerifier');
|
|
109
|
+
if (!codeVerifier) {
|
|
110
|
+
throw new Error('Code verifier not found');
|
|
210
111
|
}
|
|
211
|
-
|
|
112
|
+
|
|
113
|
+
const response = await axios.post(`${this.authServer}auth/pkce_exchange`, {
|
|
114
|
+
realm_name: this.realmName,
|
|
115
|
+
code,
|
|
116
|
+
redirect_uri: this.redirectUri,
|
|
117
|
+
code_verifier: codeVerifier,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
localStorage.setItem('access_token', response.data.access_token);
|
|
121
|
+
localStorage.setItem('refresh_token', response.data.refresh_token);
|
|
122
|
+
} finally {
|
|
123
|
+
localStorage.removeItem('codeVerifier');
|
|
124
|
+
localStorage.removeItem('codeChallenge');
|
|
125
|
+
}
|
|
212
126
|
}
|
|
213
127
|
|
|
214
128
|
public async logout(): Promise<void> {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
localStorage.removeItem('refresh_token');
|
|
220
|
-
fetch(`${this.authServer}auth/logout`, {
|
|
221
|
-
method: 'POST',
|
|
222
|
-
headers: {
|
|
223
|
-
'Content-Type': 'application/json',
|
|
224
|
-
'Authorization': `Bearer ${bearerToken}`,
|
|
225
|
-
},
|
|
226
|
-
}).then((response) => {
|
|
227
|
-
if (response.status !== 200) {
|
|
228
|
-
throw new Error('Failed to attempt logout')
|
|
229
|
-
}
|
|
230
|
-
resolve();
|
|
231
|
-
}).catch((error) => {
|
|
232
|
-
reject(error);
|
|
233
|
-
})
|
|
234
|
-
} catch (error) {
|
|
235
|
-
reject(error);
|
|
129
|
+
try {
|
|
130
|
+
const accessToken = localStorage.getItem('access_token');
|
|
131
|
+
if (!accessToken) {
|
|
132
|
+
throw new Error('Access token not found');
|
|
236
133
|
}
|
|
237
|
-
|
|
134
|
+
await axios.post(`${this.authServer}auth/logout`, {}, {
|
|
135
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
136
|
+
});
|
|
137
|
+
} finally {
|
|
138
|
+
localStorage.removeItem('access_token');
|
|
139
|
+
localStorage.removeItem('refresh_token');
|
|
140
|
+
}
|
|
238
141
|
}
|
|
239
142
|
|
|
240
143
|
public static async validateToken(authServer: string, bearerToken: string): Promise<boolean> {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const accessToken = bearerToken.includes('Bearer ') ? bearerToken.replace('Bearer ', '') : bearerToken;
|
|
244
|
-
const decodedToken = accessToken ? JSON.parse(atob(accessToken.split('.')[1])) : null;
|
|
144
|
+
try {
|
|
145
|
+
const decodedToken = jwt.decode(bearerToken, { complete: true })?.payload;
|
|
245
146
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
147
|
+
if (!decodedToken || decodedToken.exp < Date.now() / 1000) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
249
150
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return resolve(false);
|
|
253
|
-
}
|
|
151
|
+
const { data: publicKey } = await axios.get(`${authServer}public/public_key`);
|
|
152
|
+
const { data: algo } = await axios.get(`${authServer}public/algo`);
|
|
254
153
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return resolve(true);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
} catch (error) {
|
|
270
|
-
reject(error);
|
|
271
|
-
}
|
|
272
|
-
})
|
|
154
|
+
jwt.verify(bearerToken, publicKey, { algorithms: [algo] });
|
|
155
|
+
|
|
156
|
+
const { data: revokedIds } = await axios.get(`${authServer}public/revoked_ids`);
|
|
157
|
+
return !revokedIds.includes(decodedToken['id']);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public static resetInstance(): void {
|
|
164
|
+
AuthManager.instance = null;
|
|
273
165
|
}
|
|
274
166
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import MockAdapter from 'axios-mock-adapter';
|
|
3
|
+
import { AuthManager } from '../src/AuthManager';
|
|
4
|
+
|
|
5
|
+
const mock = new MockAdapter(axios);
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describe('AuthManager Tests', () => {
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
localStorage.clear(); // Clear localStorage before each test
|
|
12
|
+
AuthManager.resetInstance(); // Reset singleton instance
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
it('singleton: should throw when getting instance without initialization', () => {
|
|
18
|
+
expect(() => AuthManager.getInstance()).toThrow('AuthManager not initialized');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('singleton: should create an instance', () => {
|
|
22
|
+
const loginCallback = jest.fn();
|
|
23
|
+
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
24
|
+
expect(AuthManager.getInstance()).toBeInstanceOf(AuthManager);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('PKCE Generation: generates a PKCE pair and stores in local storage', () => {
|
|
28
|
+
const loginCallback = jest.fn();
|
|
29
|
+
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
30
|
+
// Accessing the private method by casting to any
|
|
31
|
+
const pkce = (manager as any).generatePKCEPair();
|
|
32
|
+
|
|
33
|
+
expect(pkce).toHaveProperty('verifier');
|
|
34
|
+
expect(pkce).toHaveProperty('challenge');
|
|
35
|
+
expect(pkce.verifier).toMatch(/[\w-_]+/);
|
|
36
|
+
expect(pkce.challenge).toMatch(/[\w-_]+/);
|
|
37
|
+
|
|
38
|
+
expect(pkce.verifier).toMatch(/[\w-_=]+/);
|
|
39
|
+
expect(pkce.challenge).toMatch(/[\w-_=]+/);
|
|
40
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('codeVerifier', expect.anything());
|
|
41
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('codeChallenge', expect.anything());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
it('refreshes access token when expired', async () => {
|
|
46
|
+
mock.onPost('http://auth-server.com/auth/refresh').reply(200, {
|
|
47
|
+
access_token: 'newAccessToken',
|
|
48
|
+
refresh_token: 'newRefreshToken'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const loginCallback = jest.fn();
|
|
52
|
+
// check that we set localstorage correct
|
|
53
|
+
localStorage.setItem('access_token', 'mockAccessToken');
|
|
54
|
+
localStorage.setItem('refresh_token', 'mockRefreshToken');
|
|
55
|
+
|
|
56
|
+
const refresh = localStorage.getItem('refresh_token');
|
|
57
|
+
expect(refresh).toEqual('mockRefreshToken');
|
|
58
|
+
|
|
59
|
+
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
60
|
+
const token = await manager.refreshAccessToken();
|
|
61
|
+
|
|
62
|
+
expect(token).toEqual('newAccessToken');
|
|
63
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('access_token', 'newAccessToken');
|
|
64
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('refresh_token', 'newRefreshToken');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
it('throws an error when no refresh token is found', async () => {
|
|
69
|
+
localStorage.removeItem('refresh_token');
|
|
70
|
+
|
|
71
|
+
const loginCallback = jest.fn();
|
|
72
|
+
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
73
|
+
|
|
74
|
+
await expect(manager.refreshAccessToken()).rejects.toThrow('No refresh token found');
|
|
75
|
+
await expect(loginCallback).toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('logs in using PKCE and updates local storage', async () => {
|
|
79
|
+
localStorage.setItem('codeVerifier', 'mockCodeVerifier');
|
|
80
|
+
mock.onPost('http://auth-server.com/auth/pkce_exchange').reply(200, {
|
|
81
|
+
access_token: 'validAccessToken',
|
|
82
|
+
refresh_token: 'validRefreshToken'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const loginCallback = jest.fn();
|
|
86
|
+
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
87
|
+
await manager.loginUsingPkce('mockCode');
|
|
88
|
+
|
|
89
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('access_token', 'validAccessToken');
|
|
90
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('refresh_token', 'validRefreshToken');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('logs out and clears local storage', async () => {
|
|
94
|
+
localStorage.setItem('access_token', 'validAccessToken');
|
|
95
|
+
mock.onPost('http://auth-server.com/auth/logout').reply(200);
|
|
96
|
+
|
|
97
|
+
const loginCallback = jest.fn();
|
|
98
|
+
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
99
|
+
await manager.logout();
|
|
100
|
+
|
|
101
|
+
expect(localStorage.removeItem).toHaveBeenCalledWith('access_token');
|
|
102
|
+
expect(localStorage.removeItem).toHaveBeenCalledWith('refresh_token');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
});
|