supaapps-auth 1.4.1 → 2.0.0-rc.10
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/.eslintrc +3 -0
- package/.github/workflows/ci.yml +1 -0
- package/.prettierrc +7 -0
- package/dist/AuthManager.d.ts +17 -7
- package/dist/AuthManager.js +207 -36
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +24 -0
- package/package.json +16 -4
- package/src/AuthManager.ts +387 -130
- package/src/index.ts +1 -0
- package/src/types.ts +40 -0
- package/tests/AuthManager.test.ts +41 -8
package/.eslintrc
ADDED
package/.github/workflows/ci.yml
CHANGED
package/.prettierrc
ADDED
package/dist/AuthManager.d.ts
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
|
+
import { AuthManagerEvent, Platforms, UserTokenPayload } from './types';
|
|
1
2
|
export declare class AuthManager {
|
|
2
3
|
private static instance;
|
|
3
4
|
private authServer;
|
|
4
5
|
private realmName;
|
|
5
6
|
private redirectUri;
|
|
6
|
-
private
|
|
7
|
+
private onStateChange;
|
|
7
8
|
private constructor();
|
|
8
|
-
static initialize(authServer: string, realmName: string, redirectUri: string,
|
|
9
|
+
static initialize(authServer: string, realmName: string, redirectUri: string, onStateChange: (event: AuthManagerEvent) => void): AuthManager;
|
|
9
10
|
static getInstance(): AuthManager;
|
|
11
|
+
private tokenToPayload;
|
|
10
12
|
private toBase64Url;
|
|
11
13
|
private generatePKCEPair;
|
|
12
|
-
refreshAccessToken(): Promise<string>;
|
|
13
|
-
checkAccessToken(): Promise<string>;
|
|
14
|
+
refreshAccessToken(isInitialization?: boolean): Promise<string>;
|
|
15
|
+
checkAccessToken(isInitilization?: boolean): Promise<string>;
|
|
14
16
|
private isTokenExpired;
|
|
15
|
-
mustBeLoggedIn(): Promise<
|
|
17
|
+
mustBeLoggedIn(): Promise<void>;
|
|
16
18
|
getLoginWithGoogleUri(): string;
|
|
17
19
|
isLoggedIn(): Promise<boolean>;
|
|
18
|
-
getAccessToken(): Promise<string>;
|
|
20
|
+
getAccessToken(mustBeLoggedIn?: boolean): Promise<string>;
|
|
21
|
+
platformCheck(email: string): Promise<Array<Platforms>>;
|
|
22
|
+
verifyEmail(email: string, token: string): Promise<boolean>;
|
|
23
|
+
doPassReset(email: string, token: string, newPassword: string): Promise<boolean>;
|
|
24
|
+
changeEmail(email: string): Promise<boolean>;
|
|
25
|
+
initPasswordReset(email: string): Promise<boolean>;
|
|
26
|
+
changePassword(oldPassword: string, newPassword: string, email: string): Promise<boolean>;
|
|
27
|
+
registerUsingEmail(firstName: string, lastName: string, email: string, password: string): Promise<void>;
|
|
19
28
|
private saveTokens;
|
|
29
|
+
loginUsingEmail(email: string, password: string): Promise<void>;
|
|
20
30
|
loginUsingPkce(code: string): Promise<void>;
|
|
21
31
|
logout(): Promise<void>;
|
|
22
|
-
static validateToken(authServer: string, bearerToken: string): Promise<
|
|
32
|
+
static validateToken(authServer: string, bearerToken: string): Promise<UserTokenPayload>;
|
|
23
33
|
static resetInstance(): void;
|
|
24
34
|
}
|
package/dist/AuthManager.js
CHANGED
|
@@ -13,17 +13,29 @@ exports.AuthManager = void 0;
|
|
|
13
13
|
const axios_1 = require("axios");
|
|
14
14
|
const crypto_1 = require("crypto");
|
|
15
15
|
const jsonwebtoken_1 = require("jsonwebtoken"); // Ensure jsonwebtoken is correctly imported
|
|
16
|
+
const types_1 = require("./types");
|
|
16
17
|
class AuthManager {
|
|
17
|
-
constructor(authServer, realmName, redirectUri,
|
|
18
|
+
constructor(authServer, realmName, redirectUri, onStateChange) {
|
|
18
19
|
this.authServer = authServer;
|
|
19
20
|
this.realmName = realmName;
|
|
20
21
|
this.redirectUri = redirectUri;
|
|
21
|
-
this.
|
|
22
|
+
this.onStateChange = onStateChange;
|
|
22
23
|
AuthManager.instance = this;
|
|
23
24
|
}
|
|
24
|
-
static initialize(authServer, realmName, redirectUri,
|
|
25
|
+
static initialize(authServer, realmName, redirectUri, onStateChange) {
|
|
25
26
|
if (!AuthManager.instance) {
|
|
26
|
-
AuthManager.instance = new AuthManager(authServer, realmName, redirectUri,
|
|
27
|
+
AuthManager.instance = new AuthManager(authServer, realmName, redirectUri, onStateChange);
|
|
28
|
+
AuthManager.instance
|
|
29
|
+
.checkAccessToken(true)
|
|
30
|
+
.then((token) => {
|
|
31
|
+
onStateChange({
|
|
32
|
+
type: types_1.AuthEventType.INITALIZED_IN,
|
|
33
|
+
user: AuthManager.instance.tokenToPayload(token),
|
|
34
|
+
});
|
|
35
|
+
})
|
|
36
|
+
.catch(() => {
|
|
37
|
+
onStateChange({ type: types_1.AuthEventType.INITALIZED_OUT });
|
|
38
|
+
});
|
|
27
39
|
}
|
|
28
40
|
return AuthManager.instance;
|
|
29
41
|
}
|
|
@@ -33,8 +45,14 @@ class AuthManager {
|
|
|
33
45
|
}
|
|
34
46
|
return AuthManager.instance;
|
|
35
47
|
}
|
|
48
|
+
tokenToPayload(token) {
|
|
49
|
+
return JSON.parse(atob(token.split('.')[1]));
|
|
50
|
+
}
|
|
36
51
|
toBase64Url(base64String) {
|
|
37
|
-
return base64String
|
|
52
|
+
return base64String
|
|
53
|
+
.replace(/\+/g, '-')
|
|
54
|
+
.replace(/\//g, '_')
|
|
55
|
+
.replace(/=+$/, '');
|
|
38
56
|
}
|
|
39
57
|
generatePKCEPair() {
|
|
40
58
|
var _a, _b;
|
|
@@ -45,46 +63,50 @@ class AuthManager {
|
|
|
45
63
|
return { verifier, challenge };
|
|
46
64
|
}
|
|
47
65
|
refreshAccessToken() {
|
|
48
|
-
return __awaiter(this,
|
|
66
|
+
return __awaiter(this, arguments, void 0, function* (isInitialization = false) {
|
|
49
67
|
try {
|
|
50
68
|
const refreshToken = localStorage.getItem('refresh_token');
|
|
51
69
|
if (!refreshToken) {
|
|
52
70
|
throw new Error('No refresh token found');
|
|
53
71
|
}
|
|
54
72
|
const response = yield axios_1.default.post(`${this.authServer}auth/refresh`, {
|
|
55
|
-
refresh_token: refreshToken
|
|
73
|
+
refresh_token: refreshToken,
|
|
56
74
|
});
|
|
57
|
-
|
|
58
|
-
localStorage.setItem('access_token', response.data.access_token);
|
|
59
|
-
const user = (0, jsonwebtoken_1.decode)(response.data.access_token);
|
|
60
|
-
localStorage.setItem('user', JSON.stringify(user));
|
|
75
|
+
this.saveTokens(response, true);
|
|
61
76
|
return response.data.access_token;
|
|
62
77
|
}
|
|
63
78
|
catch (error) {
|
|
64
79
|
console.error(`Refresh token error, logging out: ${error}`);
|
|
65
80
|
localStorage.removeItem('access_token');
|
|
66
81
|
localStorage.removeItem('refresh_token');
|
|
67
|
-
|
|
82
|
+
if (!isInitialization) {
|
|
83
|
+
// throw refresh fail only if not initialization
|
|
84
|
+
this.onStateChange({ type: types_1.AuthEventType.REFRESH_FAILED });
|
|
85
|
+
}
|
|
68
86
|
throw error;
|
|
69
87
|
}
|
|
70
88
|
});
|
|
71
89
|
}
|
|
72
90
|
checkAccessToken() {
|
|
73
|
-
return __awaiter(this,
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
return this.refreshAccessToken();
|
|
91
|
+
return __awaiter(this, arguments, void 0, function* (isInitilization = false) {
|
|
92
|
+
const accessToken = localStorage.getItem('access_token');
|
|
93
|
+
if (accessToken && this.isTokenExpired(accessToken)) {
|
|
94
|
+
return this.refreshAccessToken(isInitilization);
|
|
77
95
|
}
|
|
78
96
|
return accessToken;
|
|
79
97
|
});
|
|
80
98
|
}
|
|
81
99
|
isTokenExpired(token) {
|
|
82
|
-
const decoded =
|
|
100
|
+
const decoded = this.tokenToPayload(token);
|
|
83
101
|
return decoded.exp < Date.now() / 1000;
|
|
84
102
|
}
|
|
85
103
|
mustBeLoggedIn() {
|
|
86
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
-
|
|
105
|
+
if (!(yield this.isLoggedIn())) {
|
|
106
|
+
this.onStateChange({
|
|
107
|
+
type: types_1.AuthEventType.FAILED_MUST_LOGIN_CHECK,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
88
110
|
});
|
|
89
111
|
}
|
|
90
112
|
getLoginWithGoogleUri() {
|
|
@@ -103,16 +125,150 @@ class AuthManager {
|
|
|
103
125
|
});
|
|
104
126
|
}
|
|
105
127
|
getAccessToken() {
|
|
128
|
+
return __awaiter(this, arguments, void 0, function* (mustBeLoggedIn = false) {
|
|
129
|
+
try {
|
|
130
|
+
return yield this.checkAccessToken();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (mustBeLoggedIn) {
|
|
134
|
+
this.onStateChange({
|
|
135
|
+
type: types_1.AuthEventType.FAILED_MUST_LOGIN_CHECK,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
platformCheck(email) {
|
|
106
143
|
return __awaiter(this, void 0, void 0, function* () {
|
|
107
|
-
|
|
144
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/platform_check`, {
|
|
145
|
+
realm_name: this.realmName,
|
|
146
|
+
email,
|
|
147
|
+
});
|
|
148
|
+
if (response.data.error || response.data.errors) {
|
|
149
|
+
throw new Error(response.data.error || response.data.message);
|
|
150
|
+
}
|
|
151
|
+
return (response.status === 200) ? response.data : { 'platforms': [] };
|
|
108
152
|
});
|
|
109
153
|
}
|
|
110
|
-
|
|
154
|
+
verifyEmail(email, token) {
|
|
155
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/verify`, {
|
|
157
|
+
realm_name: this.realmName,
|
|
158
|
+
email,
|
|
159
|
+
token,
|
|
160
|
+
});
|
|
161
|
+
if (response.data.error || response.data.errors) {
|
|
162
|
+
throw new Error(response.data.error || response.data.message);
|
|
163
|
+
}
|
|
164
|
+
return response.status === 200;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
doPassReset(email, token, newPassword) {
|
|
168
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
169
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/do_pass_reset`, {
|
|
170
|
+
realm_name: this.realmName,
|
|
171
|
+
email,
|
|
172
|
+
token,
|
|
173
|
+
new_password: newPassword,
|
|
174
|
+
});
|
|
175
|
+
if (response.data.error || response.data.errors) {
|
|
176
|
+
throw new Error(response.data.error || response.data.message);
|
|
177
|
+
}
|
|
178
|
+
return response.status === 200;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
changeEmail(email) {
|
|
182
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
const accessToken = localStorage.getItem('access_token');
|
|
184
|
+
if (!accessToken) {
|
|
185
|
+
throw new Error('Access token not found');
|
|
186
|
+
}
|
|
187
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/change_email`, {
|
|
188
|
+
realm_name: this.realmName,
|
|
189
|
+
email,
|
|
190
|
+
}, {
|
|
191
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
192
|
+
});
|
|
193
|
+
if (response.data.error || response.data.errors) {
|
|
194
|
+
throw new Error(response.data.error || response.data.message);
|
|
195
|
+
}
|
|
196
|
+
return response.status === 200;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
initPasswordReset(email) {
|
|
200
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
201
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/init_pass_reset`, {
|
|
202
|
+
realm_name: this.realmName,
|
|
203
|
+
email,
|
|
204
|
+
});
|
|
205
|
+
if (response.data.error || response.data.errors) {
|
|
206
|
+
throw new Error(response.data.error || response.data.message);
|
|
207
|
+
}
|
|
208
|
+
return response.status === 200 || response.status === 201;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
changePassword(oldPassword, newPassword, email) {
|
|
212
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
+
const accessToken = localStorage.getItem('access_token');
|
|
214
|
+
if (!accessToken) {
|
|
215
|
+
throw new Error('Access token not found');
|
|
216
|
+
}
|
|
217
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/change_pass`, {
|
|
218
|
+
realm_name: this.realmName,
|
|
219
|
+
email,
|
|
220
|
+
old_password: oldPassword,
|
|
221
|
+
new_password: newPassword,
|
|
222
|
+
}, {
|
|
223
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
224
|
+
});
|
|
225
|
+
if (response.data.error || response.data.errors) {
|
|
226
|
+
throw new Error(response.data.error || response.data.message);
|
|
227
|
+
}
|
|
228
|
+
return response.status === 200;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
registerUsingEmail(firstName, lastName, email, password) {
|
|
232
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
233
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/register`, {
|
|
234
|
+
realm_name: this.realmName,
|
|
235
|
+
first_name: firstName,
|
|
236
|
+
last_name: lastName,
|
|
237
|
+
email,
|
|
238
|
+
password,
|
|
239
|
+
});
|
|
240
|
+
if (response.data.message || response.data.error) {
|
|
241
|
+
throw new Error(response.data.message || response.data.error);
|
|
242
|
+
}
|
|
243
|
+
if (!response.data.access_token) {
|
|
244
|
+
throw new Error('Something went wrong');
|
|
245
|
+
}
|
|
246
|
+
this.saveTokens(response, false);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
saveTokens(response, byRefresh) {
|
|
111
250
|
localStorage.setItem('access_token', response.data.access_token);
|
|
112
251
|
localStorage.setItem('refresh_token', response.data.refresh_token);
|
|
113
|
-
|
|
252
|
+
this.onStateChange({
|
|
253
|
+
type: byRefresh ? types_1.AuthEventType.USER_UPDATED : types_1.AuthEventType.USER_LOGGED_IN,
|
|
254
|
+
user: this.tokenToPayload(response.data.access_token),
|
|
255
|
+
});
|
|
256
|
+
const user = this.tokenToPayload(response.data.access_token);
|
|
114
257
|
localStorage.setItem('user', JSON.stringify(user));
|
|
115
258
|
}
|
|
259
|
+
loginUsingEmail(email, password) {
|
|
260
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
261
|
+
const response = yield axios_1.default.post(`${this.authServer}auth/email/login`, {
|
|
262
|
+
realm_name: this.realmName,
|
|
263
|
+
email,
|
|
264
|
+
password,
|
|
265
|
+
});
|
|
266
|
+
if (response.data.message || response.data.error) {
|
|
267
|
+
throw new Error(response.data.message || response.data.error);
|
|
268
|
+
}
|
|
269
|
+
this.saveTokens(response, false);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
116
272
|
loginUsingPkce(code) {
|
|
117
273
|
return __awaiter(this, void 0, void 0, function* () {
|
|
118
274
|
try {
|
|
@@ -126,7 +282,7 @@ class AuthManager {
|
|
|
126
282
|
redirect_uri: this.redirectUri,
|
|
127
283
|
code_verifier: codeVerifier,
|
|
128
284
|
});
|
|
129
|
-
this.saveTokens(response);
|
|
285
|
+
this.saveTokens(response, false);
|
|
130
286
|
}
|
|
131
287
|
finally {
|
|
132
288
|
localStorage.removeItem('codeVerifier');
|
|
@@ -142,33 +298,48 @@ class AuthManager {
|
|
|
142
298
|
throw new Error('Access token not found');
|
|
143
299
|
}
|
|
144
300
|
yield axios_1.default.post(`${this.authServer}auth/logout`, {}, {
|
|
145
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
301
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
146
302
|
});
|
|
147
303
|
}
|
|
148
304
|
finally {
|
|
149
305
|
localStorage.removeItem('access_token');
|
|
150
306
|
localStorage.removeItem('refresh_token');
|
|
307
|
+
this.onStateChange({ type: types_1.AuthEventType.USER_LOGGED_OUT });
|
|
151
308
|
}
|
|
152
309
|
});
|
|
153
310
|
}
|
|
154
311
|
static validateToken(authServer, bearerToken) {
|
|
155
|
-
var _a;
|
|
156
312
|
return __awaiter(this, void 0, void 0, function* () {
|
|
313
|
+
var _a;
|
|
157
314
|
// @todo tests missing for this static validation
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
|
|
165
|
-
(0, jsonwebtoken_1.verify)(bearerToken, publicKey, { algorithms: [algo] });
|
|
166
|
-
const { data: revokedIds } = yield axios_1.default.get(`${authServer}public/revoked_ids`);
|
|
167
|
-
return !revokedIds.includes(decodedToken['id']);
|
|
315
|
+
// @todo add caching for public key and algo
|
|
316
|
+
const decodedToken = (_a = (0, jsonwebtoken_1.decode)(bearerToken, {
|
|
317
|
+
complete: true,
|
|
318
|
+
})) === null || _a === void 0 ? void 0 : _a.payload;
|
|
319
|
+
if (!decodedToken) {
|
|
320
|
+
throw new Error('Not a valid jwt token');
|
|
168
321
|
}
|
|
169
|
-
|
|
170
|
-
|
|
322
|
+
const userToken = {
|
|
323
|
+
id: decodedToken.id,
|
|
324
|
+
iss: decodedToken.iss,
|
|
325
|
+
sub: typeof decodedToken.sub === 'string' ? parseInt(decodedToken.sub) : decodedToken.sub,
|
|
326
|
+
first_name: decodedToken.first_name,
|
|
327
|
+
last_name: decodedToken.last_name,
|
|
328
|
+
email: decodedToken.email,
|
|
329
|
+
aud: decodedToken.aud,
|
|
330
|
+
iat: decodedToken.iat,
|
|
331
|
+
exp: decodedToken.exp,
|
|
332
|
+
scopes: decodedToken.scopes,
|
|
333
|
+
realm: decodedToken.realm,
|
|
334
|
+
};
|
|
335
|
+
const { data: publicKey } = yield axios_1.default.get(`${authServer}public/public_key`);
|
|
336
|
+
const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
|
|
337
|
+
(0, jsonwebtoken_1.verify)(bearerToken, publicKey, { algorithms: [algo] });
|
|
338
|
+
const { data: revokedIds } = yield axios_1.default.get(`${authServer}public/revoked_ids`);
|
|
339
|
+
if (revokedIds.includes(decodedToken.id)) {
|
|
340
|
+
throw new Error('Token is revoked');
|
|
171
341
|
}
|
|
342
|
+
return userToken;
|
|
172
343
|
});
|
|
173
344
|
}
|
|
174
345
|
static resetInstance() {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
17
|
exports.AuthManager = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
4
19
|
var AuthManager_1 = require("./AuthManager");
|
|
5
20
|
Object.defineProperty(exports, "AuthManager", { enumerable: true, get: function () { return AuthManager_1.AuthManager; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare enum AuthEventType {
|
|
2
|
+
INITALIZED_IN = "initialized-logged-in",
|
|
3
|
+
INITALIZED_OUT = "initialized-logged-out",
|
|
4
|
+
USER_LOGGED_IN = "user-logged-in",
|
|
5
|
+
USER_LOGGED_OUT = "user-logged-out",
|
|
6
|
+
USER_UPDATED = "user-updated",
|
|
7
|
+
FAILED_MUST_LOGIN_CHECK = "failed-must-login",
|
|
8
|
+
REFRESH_FAILED = "refresh-failed"
|
|
9
|
+
}
|
|
10
|
+
export interface UserTokenPayload {
|
|
11
|
+
id: number;
|
|
12
|
+
iss: string;
|
|
13
|
+
sub: number | string;
|
|
14
|
+
first_name: string;
|
|
15
|
+
last_name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
aud: string;
|
|
18
|
+
iat: number;
|
|
19
|
+
exp: number;
|
|
20
|
+
scopes: string;
|
|
21
|
+
realm: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AuthManagerEvent {
|
|
24
|
+
type: AuthEventType;
|
|
25
|
+
user?: UserTokenPayload;
|
|
26
|
+
}
|
|
27
|
+
export declare enum Platforms {
|
|
28
|
+
PASSWORD = "password",
|
|
29
|
+
GOOGLE = "google",
|
|
30
|
+
FACEBOOK = "facebook",
|
|
31
|
+
TWITTER = "twitter",
|
|
32
|
+
GITHUB = "github",
|
|
33
|
+
APPLE = "apple",
|
|
34
|
+
LINKEDIN = "linkedin",
|
|
35
|
+
MICROSOFT = "microsoft"
|
|
36
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Platforms = exports.AuthEventType = void 0;
|
|
4
|
+
var AuthEventType;
|
|
5
|
+
(function (AuthEventType) {
|
|
6
|
+
AuthEventType["INITALIZED_IN"] = "initialized-logged-in";
|
|
7
|
+
AuthEventType["INITALIZED_OUT"] = "initialized-logged-out";
|
|
8
|
+
AuthEventType["USER_LOGGED_IN"] = "user-logged-in";
|
|
9
|
+
AuthEventType["USER_LOGGED_OUT"] = "user-logged-out";
|
|
10
|
+
AuthEventType["USER_UPDATED"] = "user-updated";
|
|
11
|
+
AuthEventType["FAILED_MUST_LOGIN_CHECK"] = "failed-must-login";
|
|
12
|
+
AuthEventType["REFRESH_FAILED"] = "refresh-failed";
|
|
13
|
+
})(AuthEventType || (exports.AuthEventType = AuthEventType = {}));
|
|
14
|
+
var Platforms;
|
|
15
|
+
(function (Platforms) {
|
|
16
|
+
Platforms["PASSWORD"] = "password";
|
|
17
|
+
Platforms["GOOGLE"] = "google";
|
|
18
|
+
Platforms["FACEBOOK"] = "facebook";
|
|
19
|
+
Platforms["TWITTER"] = "twitter";
|
|
20
|
+
Platforms["GITHUB"] = "github";
|
|
21
|
+
Platforms["APPLE"] = "apple";
|
|
22
|
+
Platforms["LINKEDIN"] = "linkedin";
|
|
23
|
+
Platforms["MICROSOFT"] = "microsoft";
|
|
24
|
+
})(Platforms || (exports.Platforms = Platforms = {}));
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supaapps-auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-rc.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest",
|
|
9
|
-
"build": "tsc"
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"lint": "eslint src/ --ext .ts,.tsx"
|
|
10
11
|
},
|
|
11
12
|
"author": "",
|
|
12
13
|
"license": "MIT",
|
|
@@ -16,15 +17,26 @@
|
|
|
16
17
|
"jsonwebtoken": "^9.0.2"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
20
|
+
"@next/eslint-plugin-next": "^13.5.6",
|
|
19
21
|
"@types/axios": "^0.14.0",
|
|
20
22
|
"@types/jest": "^29.5.12",
|
|
21
23
|
"@types/jsonwebtoken": "^9.0.6",
|
|
22
|
-
"@types/node": "^20.
|
|
24
|
+
"@types/node": "^20.12.12",
|
|
25
|
+
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
26
|
+
"@typescript-eslint/parser": "^6.21.0",
|
|
23
27
|
"axios-mock-adapter": "^1.22.0",
|
|
28
|
+
"eslint": "^8.57.1",
|
|
29
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
30
|
+
"eslint-config-airbnb-typescript": "^17.1.0",
|
|
31
|
+
"eslint-config-next": "^13.5.6",
|
|
32
|
+
"eslint-config-prettier": "^9.1.0",
|
|
33
|
+
"eslint-config-supaapps": "^1.1.0",
|
|
34
|
+
"eslint-plugin-import": "^2.29.1",
|
|
24
35
|
"jest": "^29.7.0",
|
|
25
36
|
"jest-localstorage-mock": "^2.4.26",
|
|
26
37
|
"jest-mock-axios": "^4.7.3",
|
|
27
38
|
"ts-jest": "^29.1.2",
|
|
28
|
-
"typescript": "^5.3.3"
|
|
39
|
+
"typescript": "^5.3.3",
|
|
40
|
+
"undefined": "^0.1.0"
|
|
29
41
|
}
|
|
30
42
|
}
|
package/src/AuthManager.ts
CHANGED
|
@@ -1,174 +1,431 @@
|
|
|
1
1
|
import axios, { AxiosResponse } from 'axios';
|
|
2
2
|
import { createHash, randomBytes } from 'crypto';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
decode as jwtDecode,
|
|
5
|
+
verify as jwtVerify,
|
|
6
|
+
} from 'jsonwebtoken'; // Ensure jsonwebtoken is correctly imported
|
|
7
|
+
import {AuthEventType, AuthManagerEvent, Platforms, UserTokenPayload} from './types';
|
|
4
8
|
|
|
5
9
|
export class AuthManager {
|
|
6
|
-
|
|
7
|
-
private authServer: string;
|
|
8
|
-
private realmName: string;
|
|
9
|
-
private redirectUri: string;
|
|
10
|
-
private loginCallback: () => void;
|
|
10
|
+
private static instance: AuthManager | null = null;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
private authServer: string;
|
|
13
|
+
|
|
14
|
+
private realmName: string;
|
|
15
|
+
|
|
16
|
+
private redirectUri: string;
|
|
17
|
+
|
|
18
|
+
private onStateChange: (event: AuthManagerEvent) => void;
|
|
19
|
+
|
|
20
|
+
private constructor(
|
|
21
|
+
authServer: string,
|
|
22
|
+
realmName: string,
|
|
23
|
+
redirectUri: string,
|
|
24
|
+
onStateChange: (event: AuthManagerEvent) => void,
|
|
25
|
+
) {
|
|
26
|
+
this.authServer = authServer;
|
|
27
|
+
this.realmName = realmName;
|
|
28
|
+
this.redirectUri = redirectUri;
|
|
29
|
+
this.onStateChange = onStateChange;
|
|
30
|
+
AuthManager.instance = this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static initialize(
|
|
34
|
+
authServer: string,
|
|
35
|
+
realmName: string,
|
|
36
|
+
redirectUri: string,
|
|
37
|
+
onStateChange: (event: AuthManagerEvent) => void,
|
|
38
|
+
): AuthManager {
|
|
39
|
+
if (!AuthManager.instance) {
|
|
40
|
+
AuthManager.instance = new AuthManager(
|
|
41
|
+
authServer,
|
|
42
|
+
realmName,
|
|
43
|
+
redirectUri,
|
|
44
|
+
onStateChange,
|
|
45
|
+
);
|
|
46
|
+
AuthManager.instance
|
|
47
|
+
.checkAccessToken(true)
|
|
48
|
+
.then((token) => {
|
|
49
|
+
onStateChange({
|
|
50
|
+
type: AuthEventType.INITALIZED_IN,
|
|
51
|
+
user: AuthManager.instance.tokenToPayload(token),
|
|
52
|
+
});
|
|
53
|
+
})
|
|
54
|
+
.catch(() => {
|
|
55
|
+
onStateChange({ type: AuthEventType.INITALIZED_OUT });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return AuthManager.instance;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public static getInstance(): AuthManager {
|
|
62
|
+
if (!AuthManager.instance) {
|
|
63
|
+
throw new Error('AuthManager not initialized');
|
|
18
64
|
}
|
|
65
|
+
return AuthManager.instance;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private tokenToPayload(token: string): UserTokenPayload {
|
|
69
|
+
return JSON.parse(atob(token.split('.')[1]));
|
|
70
|
+
}
|
|
19
71
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
72
|
+
private toBase64Url(base64String: string): string {
|
|
73
|
+
return base64String
|
|
74
|
+
.replace(/\+/g, '-')
|
|
75
|
+
.replace(/\//g, '_')
|
|
76
|
+
.replace(/=+$/, '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private generatePKCEPair(): {
|
|
80
|
+
verifier: string,
|
|
81
|
+
challenge: string,
|
|
82
|
+
} {
|
|
83
|
+
const verifier =
|
|
84
|
+
localStorage.getItem('codeVerifier') ??
|
|
85
|
+
this.toBase64Url(randomBytes(32).toString('base64'));
|
|
86
|
+
const challenge =
|
|
87
|
+
localStorage.getItem('codeChallenge') ??
|
|
88
|
+
this.toBase64Url(
|
|
89
|
+
createHash('sha256').update(verifier).digest('base64'),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
localStorage.setItem('codeVerifier', verifier);
|
|
93
|
+
localStorage.setItem('codeChallenge', challenge);
|
|
94
|
+
|
|
95
|
+
return { verifier, challenge };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public async refreshAccessToken(isInitialization: boolean = false): Promise<string> {
|
|
99
|
+
try {
|
|
100
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
101
|
+
if (!refreshToken) {
|
|
102
|
+
throw new Error('No refresh token found');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const response = await axios.post(
|
|
106
|
+
`${this.authServer}auth/refresh`,
|
|
107
|
+
{
|
|
108
|
+
refresh_token: refreshToken,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
this.saveTokens(response, true);
|
|
112
|
+
return response.data.access_token;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`Refresh token error, logging out: ${error}`);
|
|
115
|
+
localStorage.removeItem('access_token');
|
|
116
|
+
localStorage.removeItem('refresh_token');
|
|
117
|
+
if (!isInitialization) {
|
|
118
|
+
// throw refresh fail only if not initialization
|
|
119
|
+
this.onStateChange({ type: AuthEventType.REFRESH_FAILED });
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
25
122
|
}
|
|
123
|
+
}
|
|
26
124
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return AuthManager.instance;
|
|
125
|
+
public async checkAccessToken(isInitilization: boolean = false): Promise<string> {
|
|
126
|
+
const accessToken = localStorage.getItem('access_token');
|
|
127
|
+
if (accessToken && this.isTokenExpired(accessToken)) {
|
|
128
|
+
return this.refreshAccessToken(isInitilization);
|
|
32
129
|
}
|
|
130
|
+
return accessToken;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private isTokenExpired(token: string): boolean {
|
|
134
|
+
const decoded = this.tokenToPayload(token);
|
|
135
|
+
return decoded.exp < Date.now() / 1000;
|
|
136
|
+
}
|
|
33
137
|
|
|
34
|
-
|
|
35
|
-
|
|
138
|
+
public async mustBeLoggedIn(): Promise<void> {
|
|
139
|
+
if (!(await this.isLoggedIn())) {
|
|
140
|
+
this.onStateChange({
|
|
141
|
+
type: AuthEventType.FAILED_MUST_LOGIN_CHECK,
|
|
142
|
+
});
|
|
36
143
|
}
|
|
144
|
+
}
|
|
37
145
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
146
|
+
public getLoginWithGoogleUri(): string {
|
|
147
|
+
const { challenge } = this.generatePKCEPair();
|
|
148
|
+
return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256`;
|
|
149
|
+
}
|
|
41
150
|
|
|
42
|
-
|
|
43
|
-
|
|
151
|
+
public async isLoggedIn(): Promise<boolean> {
|
|
152
|
+
try {
|
|
153
|
+
await this.checkAccessToken();
|
|
154
|
+
return true;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
44
159
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
160
|
+
public async getAccessToken(mustBeLoggedIn: boolean = false): Promise<string> {
|
|
161
|
+
try {
|
|
162
|
+
return await this.checkAccessToken();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (mustBeLoggedIn) {
|
|
165
|
+
this.onStateChange({
|
|
166
|
+
type: AuthEventType.FAILED_MUST_LOGIN_CHECK,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
54
172
|
|
|
55
|
-
const response = await axios.post(`${this.authServer}auth/refresh`, {
|
|
56
|
-
refresh_token: refreshToken
|
|
57
|
-
});
|
|
58
173
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
174
|
+
public async platformCheck(email: string): Promise<Array<Platforms>> {
|
|
175
|
+
const response = await axios.post(
|
|
176
|
+
`${this.authServer}auth/email/platform_check`,
|
|
177
|
+
{
|
|
178
|
+
realm_name: this.realmName,
|
|
179
|
+
email,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
if (response.data.error || response.data.errors) {
|
|
183
|
+
throw new Error(response.data.error || response.data.message);
|
|
71
184
|
}
|
|
72
185
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
186
|
+
return (response.status === 200) ? response.data : {'platforms': []};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public async verifyEmail(email: string, token: string): Promise<boolean> {
|
|
190
|
+
const response = await axios.post(
|
|
191
|
+
`${this.authServer}auth/email/verify`,
|
|
192
|
+
{
|
|
193
|
+
realm_name: this.realmName,
|
|
194
|
+
email,
|
|
195
|
+
token,
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
if (response.data.error || response.data.errors) {
|
|
199
|
+
throw new Error(response.data.error || response.data.message);
|
|
79
200
|
}
|
|
80
201
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
202
|
+
return response.status === 200;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public async doPassReset(email: string, token: string, newPassword: string): Promise<boolean> {
|
|
206
|
+
const response = await axios.post(
|
|
207
|
+
`${this.authServer}auth/email/do_pass_reset`,
|
|
208
|
+
{
|
|
209
|
+
realm_name: this.realmName,
|
|
210
|
+
email,
|
|
211
|
+
token,
|
|
212
|
+
new_password: newPassword,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
if (response.data.error || response.data.errors) {
|
|
216
|
+
throw new Error(response.data.error || response.data.message);
|
|
84
217
|
}
|
|
85
218
|
|
|
86
|
-
|
|
87
|
-
|
|
219
|
+
return response.status === 200;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public async changeEmail(email: string): Promise<boolean> {
|
|
223
|
+
const accessToken = localStorage.getItem('access_token');
|
|
224
|
+
if (!accessToken) {
|
|
225
|
+
throw new Error('Access token not found');
|
|
226
|
+
}
|
|
227
|
+
const response = await axios.post(
|
|
228
|
+
`${this.authServer}auth/email/change_email`,
|
|
229
|
+
{
|
|
230
|
+
realm_name: this.realmName,
|
|
231
|
+
email,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
if (response.data.error || response.data.errors) {
|
|
238
|
+
throw new Error(response.data.error || response.data.message);
|
|
88
239
|
}
|
|
89
240
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
241
|
+
return response.status === 200;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public async initPasswordReset(email: string): Promise<boolean> {
|
|
245
|
+
const response = await axios.post(
|
|
246
|
+
`${this.authServer}auth/email/init_pass_reset`,
|
|
247
|
+
{
|
|
248
|
+
realm_name: this.realmName,
|
|
249
|
+
email,
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
if (response.data.error || response.data.errors) {
|
|
253
|
+
throw new Error(response.data.error || response.data.message);
|
|
93
254
|
}
|
|
94
255
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
256
|
+
return response.status === 200 || response.status === 201;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public async changePassword(oldPassword: string, newPassword: string, email: string): Promise<boolean> {
|
|
260
|
+
const accessToken = localStorage.getItem('access_token');
|
|
261
|
+
if (!accessToken) {
|
|
262
|
+
throw new Error('Access token not found');
|
|
102
263
|
}
|
|
264
|
+
const response = await axios.post(
|
|
265
|
+
`${this.authServer}auth/email/change_pass`,
|
|
266
|
+
{
|
|
267
|
+
realm_name: this.realmName,
|
|
268
|
+
email,
|
|
269
|
+
old_password: oldPassword,
|
|
270
|
+
new_password: newPassword,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
if (response.data.error || response.data.errors) {
|
|
277
|
+
throw new Error(response.data.error || response.data.message);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return response.status === 200;
|
|
281
|
+
}
|
|
103
282
|
|
|
104
|
-
|
|
105
|
-
|
|
283
|
+
public async registerUsingEmail(
|
|
284
|
+
firstName: string,
|
|
285
|
+
lastName: string,
|
|
286
|
+
email: string,
|
|
287
|
+
password: string
|
|
288
|
+
): Promise<void> {
|
|
289
|
+
const response = await axios.post(
|
|
290
|
+
`${this.authServer}auth/email/register`,
|
|
291
|
+
{
|
|
292
|
+
realm_name: this.realmName,
|
|
293
|
+
first_name: firstName,
|
|
294
|
+
last_name: lastName,
|
|
295
|
+
email,
|
|
296
|
+
password,
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
if (response.data.message || response.data.error) {
|
|
300
|
+
throw new Error(response.data.message || response.data.error);
|
|
106
301
|
}
|
|
107
302
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
localStorage.setItem('refresh_token', response.data.refresh_token);
|
|
111
|
-
const user = jwtDecode(response.data.access_token);
|
|
112
|
-
localStorage.setItem('user', JSON.stringify(user));
|
|
303
|
+
if (!response.data.access_token) {
|
|
304
|
+
throw new Error('Something went wrong');
|
|
113
305
|
}
|
|
114
306
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const codeVerifier = localStorage.getItem('codeVerifier');
|
|
118
|
-
if (!codeVerifier) {
|
|
119
|
-
throw new Error('Code verifier not found');
|
|
120
|
-
}
|
|
307
|
+
this.saveTokens(response, false);
|
|
308
|
+
}
|
|
121
309
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
310
|
+
private saveTokens(response: AxiosResponse, byRefresh: boolean): void {
|
|
311
|
+
localStorage.setItem('access_token', response.data.access_token);
|
|
312
|
+
localStorage.setItem(
|
|
313
|
+
'refresh_token',
|
|
314
|
+
response.data.refresh_token,
|
|
315
|
+
);
|
|
316
|
+
this.onStateChange({
|
|
317
|
+
type: byRefresh ? AuthEventType.USER_UPDATED : AuthEventType.USER_LOGGED_IN,
|
|
318
|
+
user: this.tokenToPayload(response.data.access_token),
|
|
319
|
+
});
|
|
320
|
+
const user = this.tokenToPayload(response.data.access_token);
|
|
321
|
+
localStorage.setItem('user', JSON.stringify(user));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
public async loginUsingEmail(email: string, password: string): Promise<void> {
|
|
325
|
+
const response = await axios.post(
|
|
326
|
+
`${this.authServer}auth/email/login`,
|
|
327
|
+
{
|
|
328
|
+
realm_name: this.realmName,
|
|
329
|
+
email,
|
|
330
|
+
password,
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
if (response.data.message || response.data.error) {
|
|
334
|
+
throw new Error(response.data.message || response.data.error);
|
|
133
335
|
}
|
|
336
|
+
this.saveTokens(response, false);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
public async loginUsingPkce(code: string): Promise<void> {
|
|
340
|
+
try {
|
|
341
|
+
const codeVerifier = localStorage.getItem('codeVerifier');
|
|
342
|
+
if (!codeVerifier) {
|
|
343
|
+
throw new Error('Code verifier not found');
|
|
344
|
+
}
|
|
134
345
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
public static async validateToken(authServer: string, bearerToken: string): Promise<boolean> {
|
|
151
|
-
// @todo tests missing for this static validation
|
|
152
|
-
try {
|
|
153
|
-
const decodedToken = jwtDecode(bearerToken, { complete: true })?.payload;
|
|
346
|
+
const response = await axios.post(
|
|
347
|
+
`${this.authServer}auth/pkce_exchange`,
|
|
348
|
+
{
|
|
349
|
+
realm_name: this.realmName,
|
|
350
|
+
code,
|
|
351
|
+
redirect_uri: this.redirectUri,
|
|
352
|
+
code_verifier: codeVerifier,
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
this.saveTokens(response, false);
|
|
356
|
+
} finally {
|
|
357
|
+
localStorage.removeItem('codeVerifier');
|
|
358
|
+
localStorage.removeItem('codeChallenge');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
154
361
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
362
|
+
public async logout(): Promise<void> {
|
|
363
|
+
try {
|
|
364
|
+
const accessToken = localStorage.getItem('access_token');
|
|
365
|
+
if (!accessToken) {
|
|
366
|
+
throw new Error('Access token not found');
|
|
367
|
+
}
|
|
368
|
+
await axios.post(
|
|
369
|
+
`${this.authServer}auth/logout`,
|
|
370
|
+
{},
|
|
371
|
+
{
|
|
372
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
} finally {
|
|
376
|
+
localStorage.removeItem('access_token');
|
|
377
|
+
localStorage.removeItem('refresh_token');
|
|
378
|
+
this.onStateChange({ type: AuthEventType.USER_LOGGED_OUT });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
158
381
|
|
|
159
|
-
|
|
160
|
-
|
|
382
|
+
public static async validateToken(
|
|
383
|
+
authServer: string,
|
|
384
|
+
bearerToken: string,
|
|
385
|
+
): Promise<UserTokenPayload> {
|
|
386
|
+
// @todo tests missing for this static validation
|
|
387
|
+
// @todo add caching for public key and algo
|
|
388
|
+
const decodedToken = jwtDecode(bearerToken, {
|
|
389
|
+
complete: true,
|
|
390
|
+
})?.payload as unknown as UserTokenPayload;
|
|
161
391
|
|
|
162
|
-
|
|
392
|
+
if (!decodedToken) {
|
|
393
|
+
throw new Error('Not a valid jwt token');
|
|
394
|
+
}
|
|
163
395
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
396
|
+
const userToken: UserTokenPayload = {
|
|
397
|
+
id: decodedToken.id,
|
|
398
|
+
iss: decodedToken.iss,
|
|
399
|
+
sub: typeof decodedToken.sub === 'string' ? parseInt(decodedToken.sub) : decodedToken.sub,
|
|
400
|
+
first_name: decodedToken.first_name,
|
|
401
|
+
last_name: decodedToken.last_name,
|
|
402
|
+
email: decodedToken.email,
|
|
403
|
+
aud: decodedToken.aud,
|
|
404
|
+
iat: decodedToken.iat,
|
|
405
|
+
exp: decodedToken.exp,
|
|
406
|
+
scopes: decodedToken.scopes,
|
|
407
|
+
realm: decodedToken.realm,
|
|
169
408
|
}
|
|
170
409
|
|
|
171
|
-
|
|
172
|
-
|
|
410
|
+
const { data: publicKey } = await axios.get(
|
|
411
|
+
`${authServer}public/public_key`,
|
|
412
|
+
);
|
|
413
|
+
const { data: algo } = await axios.get(
|
|
414
|
+
`${authServer}public/algo`,
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
jwtVerify(bearerToken, publicKey, { algorithms: [algo] });
|
|
418
|
+
|
|
419
|
+
const { data: revokedIds } = await axios.get(
|
|
420
|
+
`${authServer}public/revoked_ids`,
|
|
421
|
+
);
|
|
422
|
+
if(revokedIds.includes(decodedToken.id)){
|
|
423
|
+
throw new Error('Token is revoked');
|
|
173
424
|
}
|
|
425
|
+
return userToken;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
public static resetInstance(): void {
|
|
429
|
+
AuthManager.instance = null;
|
|
430
|
+
}
|
|
174
431
|
}
|
package/src/index.ts
CHANGED
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
export enum AuthEventType {
|
|
3
|
+
INITALIZED_IN = 'initialized-logged-in',
|
|
4
|
+
INITALIZED_OUT = 'initialized-logged-out',
|
|
5
|
+
USER_LOGGED_IN = 'user-logged-in',
|
|
6
|
+
USER_LOGGED_OUT = 'user-logged-out',
|
|
7
|
+
USER_UPDATED = 'user-updated',
|
|
8
|
+
FAILED_MUST_LOGIN_CHECK = 'failed-must-login',
|
|
9
|
+
REFRESH_FAILED = 'refresh-failed',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UserTokenPayload {
|
|
13
|
+
id: number;
|
|
14
|
+
iss: string;
|
|
15
|
+
sub: number | string;
|
|
16
|
+
first_name: string;
|
|
17
|
+
last_name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
aud: string;
|
|
20
|
+
iat: number;
|
|
21
|
+
exp: number;
|
|
22
|
+
scopes: string;
|
|
23
|
+
realm: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AuthManagerEvent {
|
|
27
|
+
type: AuthEventType;
|
|
28
|
+
user?: UserTokenPayload;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export enum Platforms {
|
|
32
|
+
PASSWORD = 'password',
|
|
33
|
+
GOOGLE = 'google',
|
|
34
|
+
FACEBOOK = 'facebook',
|
|
35
|
+
TWITTER = 'twitter',
|
|
36
|
+
GITHUB = 'github',
|
|
37
|
+
APPLE = 'apple',
|
|
38
|
+
LINKEDIN = 'linkedin',
|
|
39
|
+
MICROSOFT = 'microsoft',
|
|
40
|
+
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import MockAdapter from 'axios-mock-adapter';
|
|
3
3
|
import { AuthManager } from '../src/AuthManager';
|
|
4
|
+
import { AuthEventType } from '../src/types';
|
|
4
5
|
import { basename } from 'path';
|
|
5
6
|
|
|
6
7
|
const mock = new MockAdapter(axios);
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
const tokenThatWontExpire1 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZmlyc3RfbmFtZSI6IkpvaG4gRG9lIiwibGFzdF9uYW1lIjoiRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZXMiOiIvcm9vdC8qIiwiZXhwIjo5OTk5OTk5OTk5LCJpZCI6MiwiaXNzIjoxMjMsImF1ZCI6InRlc3RpbmcifQ.843X4Zq2WgNSu8fjRKx-kd_FbDqY_eVkgu2wZZbhhwE';
|
|
11
|
+
const tokenThatWontExpire2 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZmlyc3RfbmFtZSI6IkpvaG4gRG9lIiwibGFzdF9uYW1lIjoiRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZXMiOiIvcm9vdC8qIiwiZXhwIjo5OTk5OTk5OTk5LCJpZCI6MiwiaXNzIjoxMjMsImF1ZCI6InRlc3RpbmcifQ.843X4Zq2WgNSu8fjRKx-kd_FbDqY_eVkgu2wZZbhhwE';
|
|
12
|
+
const tokenThatExpired = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZmlyc3RfbmFtZSI6IkpvaG4gRG9lIiwibGFzdF9uYW1lIjoiRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZXMiOiIvcm9vdC8qIiwiZXhwIjo1MDAsImlkIjoyLCJpc3MiOjEyMywiYXVkIjoidGVzdGluZyJ9.ungpbhHfCM5ZP5oiZ1RnMkJ-NKJI8s3_IPJptjyKHR4';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
describe('AuthManager Tests', () => {
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
jest.spyOn(localStorage, 'getItem');
|
|
20
|
+
});
|
|
10
21
|
|
|
11
22
|
beforeEach(() => {
|
|
12
23
|
localStorage.clear(); // Clear localStorage before each test
|
|
@@ -45,13 +56,13 @@ describe('AuthManager Tests', () => {
|
|
|
45
56
|
|
|
46
57
|
it('refreshes access token when expired', async () => {
|
|
47
58
|
mock.onPost('http://auth-server.com/auth/refresh').reply(200, {
|
|
48
|
-
access_token:
|
|
59
|
+
access_token: tokenThatWontExpire2,
|
|
49
60
|
refresh_token: 'newRefreshToken'
|
|
50
61
|
});
|
|
51
62
|
|
|
52
63
|
const loginCallback = jest.fn();
|
|
53
64
|
// check that we set localstorage correct
|
|
54
|
-
localStorage.setItem('access_token',
|
|
65
|
+
localStorage.setItem('access_token', tokenThatExpired);
|
|
55
66
|
localStorage.setItem('refresh_token', 'mockRefreshToken');
|
|
56
67
|
|
|
57
68
|
const refresh = localStorage.getItem('refresh_token');
|
|
@@ -60,12 +71,32 @@ describe('AuthManager Tests', () => {
|
|
|
60
71
|
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
61
72
|
const token = await manager.refreshAccessToken();
|
|
62
73
|
|
|
63
|
-
expect(token).toEqual(
|
|
64
|
-
expect(localStorage.setItem).toHaveBeenCalledWith('access_token',
|
|
74
|
+
expect(token).toEqual(tokenThatWontExpire2);
|
|
75
|
+
expect(localStorage.setItem).toHaveBeenCalledWith('access_token', tokenThatWontExpire2);
|
|
65
76
|
expect(localStorage.setItem).toHaveBeenCalledWith('refresh_token', 'newRefreshToken');
|
|
66
77
|
});
|
|
67
78
|
|
|
68
79
|
|
|
80
|
+
describe('AuthManager Tests isolated ', () => {
|
|
81
|
+
it('doesn\'t refresh access token when its not expired', async () => {
|
|
82
|
+
const stateChange = jest.fn();
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
// check that we set localstorage correct
|
|
86
|
+
localStorage.setItem('access_token', tokenThatWontExpire1);
|
|
87
|
+
localStorage.setItem('refresh_token', 'mockRefreshToken');
|
|
88
|
+
|
|
89
|
+
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', stateChange);
|
|
90
|
+
|
|
91
|
+
const currentCallCount = (localStorage.getItem as jest.Mock).mock.calls.length;
|
|
92
|
+
|
|
93
|
+
const token = await manager.getAccessToken();
|
|
94
|
+
|
|
95
|
+
expect(localStorage.getItem).toHaveBeenCalledTimes(currentCallCount + 1);
|
|
96
|
+
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
69
100
|
it('throws an error when no refresh token is found', async () => {
|
|
70
101
|
localStorage.removeItem('refresh_token');
|
|
71
102
|
|
|
@@ -73,7 +104,9 @@ describe('AuthManager Tests', () => {
|
|
|
73
104
|
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
74
105
|
|
|
75
106
|
await expect(manager.refreshAccessToken()).rejects.toThrow('No refresh token found');
|
|
76
|
-
await expect(loginCallback).
|
|
107
|
+
await expect(loginCallback).toHaveBeenCalledWith({
|
|
108
|
+
type: AuthEventType.REFRESH_FAILED,
|
|
109
|
+
});
|
|
77
110
|
});
|
|
78
111
|
|
|
79
112
|
it('logs in using PKCE and updates local storage', async () => {
|
|
@@ -104,11 +137,11 @@ describe('AuthManager Tests', () => {
|
|
|
104
137
|
});
|
|
105
138
|
|
|
106
139
|
it('logs out and clears local storage', async () => {
|
|
107
|
-
localStorage.setItem('access_token', 'validAccessToken');
|
|
108
140
|
mock.onPost('http://auth-server.com/auth/logout').reply(200);
|
|
109
|
-
|
|
141
|
+
|
|
110
142
|
const loginCallback = jest.fn();
|
|
111
143
|
const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
|
|
144
|
+
localStorage.setItem('access_token', tokenThatWontExpire1);
|
|
112
145
|
await manager.logout();
|
|
113
146
|
|
|
114
147
|
expect(localStorage.removeItem).toHaveBeenCalledWith('access_token');
|
|
@@ -117,4 +150,4 @@ describe('AuthManager Tests', () => {
|
|
|
117
150
|
|
|
118
151
|
|
|
119
152
|
|
|
120
|
-
});
|
|
153
|
+
});
|