supaapps-auth 2.0.0-rc.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/dist/AuthManager.d.ts +12 -4
- package/dist/AuthManager.js +165 -31
- package/dist/types.d.ts +12 -1
- package/dist/types.js +12 -1
- package/package.json +2 -2
- package/src/AuthManager.ts +199 -31
- package/src/types.ts +13 -1
- package/tests/AuthManager.test.ts +26 -1
package/dist/AuthManager.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthManagerEvent } from './types';
|
|
1
|
+
import { AuthManagerEvent, Platforms, UserTokenPayload } from './types';
|
|
2
2
|
export declare class AuthManager {
|
|
3
3
|
private static instance;
|
|
4
4
|
private authServer;
|
|
@@ -11,16 +11,24 @@ export declare class AuthManager {
|
|
|
11
11
|
private tokenToPayload;
|
|
12
12
|
private toBase64Url;
|
|
13
13
|
private generatePKCEPair;
|
|
14
|
-
refreshAccessToken(): Promise<string>;
|
|
15
|
-
checkAccessToken(): Promise<string>;
|
|
14
|
+
refreshAccessToken(isInitialization?: boolean): Promise<string>;
|
|
15
|
+
checkAccessToken(isInitilization?: boolean): Promise<string>;
|
|
16
16
|
private isTokenExpired;
|
|
17
17
|
mustBeLoggedIn(): Promise<void>;
|
|
18
18
|
getLoginWithGoogleUri(): string;
|
|
19
19
|
isLoggedIn(): Promise<boolean>;
|
|
20
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>;
|
|
21
28
|
private saveTokens;
|
|
29
|
+
loginUsingEmail(email: string, password: string): Promise<void>;
|
|
22
30
|
loginUsingPkce(code: string): Promise<void>;
|
|
23
31
|
logout(): Promise<void>;
|
|
24
|
-
static validateToken(authServer: string, bearerToken: string): Promise<
|
|
32
|
+
static validateToken(authServer: string, bearerToken: string): Promise<UserTokenPayload>;
|
|
25
33
|
static resetInstance(): void;
|
|
26
34
|
}
|
package/dist/AuthManager.js
CHANGED
|
@@ -25,18 +25,18 @@ class AuthManager {
|
|
|
25
25
|
static initialize(authServer, realmName, redirectUri, onStateChange) {
|
|
26
26
|
if (!AuthManager.instance) {
|
|
27
27
|
AuthManager.instance = new AuthManager(authServer, realmName, redirectUri, onStateChange);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 });
|
|
35
38
|
});
|
|
36
|
-
}
|
|
37
|
-
.catch(() => {
|
|
38
|
-
onStateChange({ type: types_1.AuthEventType.INITALIZED_OUT });
|
|
39
|
-
});
|
|
39
|
+
}
|
|
40
40
|
return AuthManager.instance;
|
|
41
41
|
}
|
|
42
42
|
static getInstance() {
|
|
@@ -63,7 +63,7 @@ class AuthManager {
|
|
|
63
63
|
return { verifier, challenge };
|
|
64
64
|
}
|
|
65
65
|
refreshAccessToken() {
|
|
66
|
-
return __awaiter(this,
|
|
66
|
+
return __awaiter(this, arguments, void 0, function* (isInitialization = false) {
|
|
67
67
|
try {
|
|
68
68
|
const refreshToken = localStorage.getItem('refresh_token');
|
|
69
69
|
if (!refreshToken) {
|
|
@@ -79,16 +79,19 @@ class AuthManager {
|
|
|
79
79
|
console.error(`Refresh token error, logging out: ${error}`);
|
|
80
80
|
localStorage.removeItem('access_token');
|
|
81
81
|
localStorage.removeItem('refresh_token');
|
|
82
|
-
|
|
82
|
+
if (!isInitialization) {
|
|
83
|
+
// throw refresh fail only if not initialization
|
|
84
|
+
this.onStateChange({ type: types_1.AuthEventType.REFRESH_FAILED });
|
|
85
|
+
}
|
|
83
86
|
throw error;
|
|
84
87
|
}
|
|
85
88
|
});
|
|
86
89
|
}
|
|
87
90
|
checkAccessToken() {
|
|
88
|
-
return __awaiter(this,
|
|
91
|
+
return __awaiter(this, arguments, void 0, function* (isInitilization = false) {
|
|
89
92
|
const accessToken = localStorage.getItem('access_token');
|
|
90
|
-
if (accessToken
|
|
91
|
-
return this.refreshAccessToken();
|
|
93
|
+
if (accessToken && this.isTokenExpired(accessToken)) {
|
|
94
|
+
return this.refreshAccessToken(isInitilization);
|
|
92
95
|
}
|
|
93
96
|
return accessToken;
|
|
94
97
|
});
|
|
@@ -136,6 +139,113 @@ class AuthManager {
|
|
|
136
139
|
}
|
|
137
140
|
});
|
|
138
141
|
}
|
|
142
|
+
platformCheck(email) {
|
|
143
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
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': [] };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
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
|
+
}
|
|
139
249
|
saveTokens(response, byRefresh) {
|
|
140
250
|
localStorage.setItem('access_token', response.data.access_token);
|
|
141
251
|
localStorage.setItem('refresh_token', response.data.refresh_token);
|
|
@@ -146,6 +256,19 @@ class AuthManager {
|
|
|
146
256
|
const user = this.tokenToPayload(response.data.access_token);
|
|
147
257
|
localStorage.setItem('user', JSON.stringify(user));
|
|
148
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
|
+
}
|
|
149
272
|
loginUsingPkce(code) {
|
|
150
273
|
return __awaiter(this, void 0, void 0, function* () {
|
|
151
274
|
try {
|
|
@@ -189,23 +312,34 @@ class AuthManager {
|
|
|
189
312
|
return __awaiter(this, void 0, void 0, function* () {
|
|
190
313
|
var _a;
|
|
191
314
|
// @todo tests missing for this static validation
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
const { data: publicKey } = yield axios_1.default.get(`${authServer}public/public_key`);
|
|
200
|
-
const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
|
|
201
|
-
(0, jsonwebtoken_1.verify)(bearerToken, publicKey, { algorithms: [algo] });
|
|
202
|
-
const { data: revokedIds } = yield axios_1.default.get(`${authServer}public/revoked_ids`);
|
|
203
|
-
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
204
|
-
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');
|
|
205
321
|
}
|
|
206
|
-
|
|
207
|
-
|
|
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');
|
|
208
341
|
}
|
|
342
|
+
return userToken;
|
|
209
343
|
});
|
|
210
344
|
}
|
|
211
345
|
static resetInstance() {
|
package/dist/types.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare enum AuthEventType {
|
|
|
10
10
|
export interface UserTokenPayload {
|
|
11
11
|
id: number;
|
|
12
12
|
iss: string;
|
|
13
|
-
sub: number;
|
|
13
|
+
sub: number | string;
|
|
14
14
|
first_name: string;
|
|
15
15
|
last_name: string;
|
|
16
16
|
email: string;
|
|
@@ -18,8 +18,19 @@ export interface UserTokenPayload {
|
|
|
18
18
|
iat: number;
|
|
19
19
|
exp: number;
|
|
20
20
|
scopes: string;
|
|
21
|
+
realm: string;
|
|
21
22
|
}
|
|
22
23
|
export interface AuthManagerEvent {
|
|
23
24
|
type: AuthEventType;
|
|
24
25
|
user?: UserTokenPayload;
|
|
25
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AuthEventType = void 0;
|
|
3
|
+
exports.Platforms = exports.AuthEventType = void 0;
|
|
4
4
|
var AuthEventType;
|
|
5
5
|
(function (AuthEventType) {
|
|
6
6
|
AuthEventType["INITALIZED_IN"] = "initialized-logged-in";
|
|
@@ -11,3 +11,14 @@ var AuthEventType;
|
|
|
11
11
|
AuthEventType["FAILED_MUST_LOGIN_CHECK"] = "failed-must-login";
|
|
12
12
|
AuthEventType["REFRESH_FAILED"] = "refresh-failed";
|
|
13
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supaapps-auth",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
26
26
|
"@typescript-eslint/parser": "^6.21.0",
|
|
27
27
|
"axios-mock-adapter": "^1.22.0",
|
|
28
|
-
"eslint": "^8.57.
|
|
28
|
+
"eslint": "^8.57.1",
|
|
29
29
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
30
30
|
"eslint-config-airbnb-typescript": "^17.1.0",
|
|
31
31
|
"eslint-config-next": "^13.5.6",
|
package/src/AuthManager.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
decode as jwtDecode,
|
|
5
5
|
verify as jwtVerify,
|
|
6
6
|
} from 'jsonwebtoken'; // Ensure jsonwebtoken is correctly imported
|
|
7
|
-
import {
|
|
7
|
+
import {AuthEventType, AuthManagerEvent, Platforms, UserTokenPayload} from './types';
|
|
8
8
|
|
|
9
9
|
export class AuthManager {
|
|
10
10
|
private static instance: AuthManager | null = null;
|
|
@@ -43,9 +43,8 @@ export class AuthManager {
|
|
|
43
43
|
redirectUri,
|
|
44
44
|
onStateChange,
|
|
45
45
|
);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.checkAccessToken()
|
|
46
|
+
AuthManager.instance
|
|
47
|
+
.checkAccessToken(true)
|
|
49
48
|
.then((token) => {
|
|
50
49
|
onStateChange({
|
|
51
50
|
type: AuthEventType.INITALIZED_IN,
|
|
@@ -55,6 +54,7 @@ export class AuthManager {
|
|
|
55
54
|
.catch(() => {
|
|
56
55
|
onStateChange({ type: AuthEventType.INITALIZED_OUT });
|
|
57
56
|
});
|
|
57
|
+
}
|
|
58
58
|
return AuthManager.instance;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -95,7 +95,7 @@ export class AuthManager {
|
|
|
95
95
|
return { verifier, challenge };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
public async refreshAccessToken(): Promise<string> {
|
|
98
|
+
public async refreshAccessToken(isInitialization: boolean = false): Promise<string> {
|
|
99
99
|
try {
|
|
100
100
|
const refreshToken = localStorage.getItem('refresh_token');
|
|
101
101
|
if (!refreshToken) {
|
|
@@ -114,15 +114,18 @@ export class AuthManager {
|
|
|
114
114
|
console.error(`Refresh token error, logging out: ${error}`);
|
|
115
115
|
localStorage.removeItem('access_token');
|
|
116
116
|
localStorage.removeItem('refresh_token');
|
|
117
|
-
|
|
117
|
+
if (!isInitialization) {
|
|
118
|
+
// throw refresh fail only if not initialization
|
|
119
|
+
this.onStateChange({ type: AuthEventType.REFRESH_FAILED });
|
|
120
|
+
}
|
|
118
121
|
throw error;
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
public async checkAccessToken(): Promise<string> {
|
|
125
|
+
public async checkAccessToken(isInitilization: boolean = false): Promise<string> {
|
|
123
126
|
const accessToken = localStorage.getItem('access_token');
|
|
124
|
-
if (accessToken
|
|
125
|
-
return this.refreshAccessToken();
|
|
127
|
+
if (accessToken && this.isTokenExpired(accessToken)) {
|
|
128
|
+
return this.refreshAccessToken(isInitilization);
|
|
126
129
|
}
|
|
127
130
|
return accessToken;
|
|
128
131
|
}
|
|
@@ -167,6 +170,143 @@ export class AuthManager {
|
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
|
|
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);
|
|
184
|
+
}
|
|
185
|
+
|
|
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);
|
|
200
|
+
}
|
|
201
|
+
|
|
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);
|
|
217
|
+
}
|
|
218
|
+
|
|
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);
|
|
239
|
+
}
|
|
240
|
+
|
|
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);
|
|
254
|
+
}
|
|
255
|
+
|
|
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');
|
|
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
|
+
}
|
|
282
|
+
|
|
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);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!response.data.access_token) {
|
|
304
|
+
throw new Error('Something went wrong');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.saveTokens(response, false);
|
|
308
|
+
}
|
|
309
|
+
|
|
170
310
|
private saveTokens(response: AxiosResponse, byRefresh: boolean): void {
|
|
171
311
|
localStorage.setItem('access_token', response.data.access_token);
|
|
172
312
|
localStorage.setItem(
|
|
@@ -181,6 +321,21 @@ export class AuthManager {
|
|
|
181
321
|
localStorage.setItem('user', JSON.stringify(user));
|
|
182
322
|
}
|
|
183
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);
|
|
335
|
+
}
|
|
336
|
+
this.saveTokens(response, false);
|
|
337
|
+
}
|
|
338
|
+
|
|
184
339
|
public async loginUsingPkce(code: string): Promise<void> {
|
|
185
340
|
try {
|
|
186
341
|
const codeVerifier = localStorage.getItem('codeVerifier');
|
|
@@ -227,34 +382,47 @@ export class AuthManager {
|
|
|
227
382
|
public static async validateToken(
|
|
228
383
|
authServer: string,
|
|
229
384
|
bearerToken: string,
|
|
230
|
-
): Promise<
|
|
385
|
+
): Promise<UserTokenPayload> {
|
|
231
386
|
// @todo tests missing for this static validation
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
387
|
+
// @todo add caching for public key and algo
|
|
388
|
+
const decodedToken = jwtDecode(bearerToken, {
|
|
389
|
+
complete: true,
|
|
390
|
+
})?.payload as unknown as UserTokenPayload;
|
|
236
391
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
392
|
+
if (!decodedToken) {
|
|
393
|
+
throw new Error('Not a valid jwt token');
|
|
394
|
+
}
|
|
240
395
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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,
|
|
408
|
+
}
|
|
247
409
|
|
|
248
|
-
|
|
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
|
+
);
|
|
249
416
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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');
|
|
257
424
|
}
|
|
425
|
+
return userToken;
|
|
258
426
|
}
|
|
259
427
|
|
|
260
428
|
public static resetInstance(): void {
|
package/src/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ export enum AuthEventType {
|
|
|
12
12
|
export interface UserTokenPayload {
|
|
13
13
|
id: number;
|
|
14
14
|
iss: string;
|
|
15
|
-
sub: number;
|
|
15
|
+
sub: number | string;
|
|
16
16
|
first_name: string;
|
|
17
17
|
last_name: string;
|
|
18
18
|
email: string;
|
|
@@ -20,9 +20,21 @@ export interface UserTokenPayload {
|
|
|
20
20
|
iat: number;
|
|
21
21
|
exp: number;
|
|
22
22
|
scopes: string;
|
|
23
|
+
realm: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface AuthManagerEvent {
|
|
26
27
|
type: AuthEventType;
|
|
27
28
|
user?: UserTokenPayload;
|
|
28
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
|
+
}
|
|
@@ -12,7 +12,12 @@ const tokenThatWontExpire2 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxM
|
|
|
12
12
|
const tokenThatExpired = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZmlyc3RfbmFtZSI6IkpvaG4gRG9lIiwibGFzdF9uYW1lIjoiRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZXMiOiIvcm9vdC8qIiwiZXhwIjo1MDAsImlkIjoyLCJpc3MiOjEyMywiYXVkIjoidGVzdGluZyJ9.ungpbhHfCM5ZP5oiZ1RnMkJ-NKJI8s3_IPJptjyKHR4';
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
|
|
15
17
|
describe('AuthManager Tests', () => {
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
jest.spyOn(localStorage, 'getItem');
|
|
20
|
+
});
|
|
16
21
|
|
|
17
22
|
beforeEach(() => {
|
|
18
23
|
localStorage.clear(); // Clear localStorage before each test
|
|
@@ -72,6 +77,26 @@ describe('AuthManager Tests', () => {
|
|
|
72
77
|
});
|
|
73
78
|
|
|
74
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
|
+
|
|
75
100
|
it('throws an error when no refresh token is found', async () => {
|
|
76
101
|
localStorage.removeItem('refresh_token');
|
|
77
102
|
|
|
@@ -125,4 +150,4 @@ describe('AuthManager Tests', () => {
|
|
|
125
150
|
|
|
126
151
|
|
|
127
152
|
|
|
128
|
-
});
|
|
153
|
+
});
|