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 ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "supaapps"
3
+ }
@@ -21,6 +21,7 @@ jobs:
21
21
  node-version: ${{ matrix.node-version }}
22
22
  cache: 'npm'
23
23
  - run: npm ci
24
+ - run: npm run lint
24
25
  - run: npm run build --if-present
25
26
  - run: npm run test
26
27
 
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "all",
4
+ "singleQuote": true,
5
+ "printWidth": 70,
6
+ "tabWidth": 2
7
+ }
@@ -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 loginCallback;
7
+ private onStateChange;
7
8
  private constructor();
8
- static initialize(authServer: string, realmName: string, redirectUri: string, loginCallback: () => void): AuthManager;
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<boolean>;
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<boolean>;
32
+ static validateToken(authServer: string, bearerToken: string): Promise<UserTokenPayload>;
23
33
  static resetInstance(): void;
24
34
  }
@@ -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, loginCallback) {
18
+ constructor(authServer, realmName, redirectUri, onStateChange) {
18
19
  this.authServer = authServer;
19
20
  this.realmName = realmName;
20
21
  this.redirectUri = redirectUri;
21
- this.loginCallback = loginCallback;
22
+ this.onStateChange = onStateChange;
22
23
  AuthManager.instance = this;
23
24
  }
24
- static initialize(authServer, realmName, redirectUri, loginCallback) {
25
+ static initialize(authServer, realmName, redirectUri, onStateChange) {
25
26
  if (!AuthManager.instance) {
26
- AuthManager.instance = new AuthManager(authServer, realmName, redirectUri, loginCallback);
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.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
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, void 0, void 0, function* () {
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
- localStorage.setItem('refresh_token', response.data.refresh_token);
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
- this.loginCallback();
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, void 0, void 0, function* () {
74
- let accessToken = localStorage.getItem('access_token');
75
- if (!accessToken || this.isTokenExpired(accessToken)) {
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 = JSON.parse(atob(token.split('.')[1]));
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
- return this.isLoggedIn() || (this.loginCallback(), false);
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
- return this.checkAccessToken();
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
- saveTokens(response) {
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
- const user = (0, jsonwebtoken_1.decode)(response.data.access_token);
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
- try {
159
- const decodedToken = (_a = (0, jsonwebtoken_1.decode)(bearerToken, { complete: true })) === null || _a === void 0 ? void 0 : _a.payload;
160
- if (!decodedToken) {
161
- return false;
162
- }
163
- const { data: publicKey } = yield axios_1.default.get(`${authServer}public/public_key`);
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
- catch (error) {
170
- return false;
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
@@ -1 +1,2 @@
1
+ export * from './types';
1
2
  export { AuthManager } from './AuthManager';
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; } });
@@ -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": "1.4.1",
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.11.10",
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
  }
@@ -1,174 +1,431 @@
1
1
  import axios, { AxiosResponse } from 'axios';
2
2
  import { createHash, randomBytes } from 'crypto';
3
- import {decode as jwtDecode, verify as jwtVerify} from 'jsonwebtoken'; // Ensure jsonwebtoken is correctly imported
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
- private static instance: AuthManager | null = null;
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
- private constructor(authServer: string, realmName: string, redirectUri: string, loginCallback: () => void) {
13
- this.authServer = authServer;
14
- this.realmName = realmName;
15
- this.redirectUri = redirectUri;
16
- this.loginCallback = loginCallback;
17
- AuthManager.instance = this;
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
- 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;
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
- public static getInstance(): AuthManager {
28
- if (!AuthManager.instance) {
29
- throw new Error('AuthManager not initialized');
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
- private toBase64Url(base64String: string): string {
35
- return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
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
- 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'));
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
- localStorage.setItem('codeVerifier', verifier);
43
- localStorage.setItem('codeChallenge', challenge);
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
- return { verifier, challenge };
46
- }
47
-
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');
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
- localStorage.setItem('refresh_token', response.data.refresh_token);
60
- localStorage.setItem('access_token', response.data.access_token);
61
- const user = jwtDecode(response.data.access_token);
62
- localStorage.setItem('user', JSON.stringify(user));
63
- return response.data.access_token;
64
- } catch (error) {
65
- console.error(`Refresh token error, logging out: ${error}`);
66
- localStorage.removeItem('access_token');
67
- localStorage.removeItem('refresh_token');
68
- this.loginCallback();
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
- public async checkAccessToken(): Promise<string> {
74
- let accessToken = localStorage.getItem('access_token');
75
- if (!accessToken || this.isTokenExpired(accessToken)) {
76
- return this.refreshAccessToken();
77
- }
78
- return accessToken;
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
- private isTokenExpired(token: string): boolean {
82
- const decoded = JSON.parse(atob(token.split('.')[1]));
83
- return decoded.exp < Date.now() / 1000;
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
- public async mustBeLoggedIn(): Promise<boolean> {
87
- return this.isLoggedIn() || (this.loginCallback(), false);
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
- public getLoginWithGoogleUri(): string {
91
- const { challenge } = this.generatePKCEPair();
92
- return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256`;
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
- public async isLoggedIn(): Promise<boolean> {
96
- try {
97
- await this.checkAccessToken();
98
- return true;
99
- } catch (error) {
100
- return false;
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
- public async getAccessToken(): Promise<string> {
105
- return this.checkAccessToken();
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
- private saveTokens(response: AxiosResponse): void {
109
- localStorage.setItem('access_token', response.data.access_token);
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
- public async loginUsingPkce(code: string): Promise<void> {
116
- try {
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
- const response = await axios.post(`${this.authServer}auth/pkce_exchange`, {
123
- realm_name: this.realmName,
124
- code,
125
- redirect_uri: this.redirectUri,
126
- code_verifier: codeVerifier,
127
- });
128
- this.saveTokens(response);
129
- } finally {
130
- localStorage.removeItem('codeVerifier');
131
- localStorage.removeItem('codeChallenge');
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
- public async logout(): Promise<void> {
136
- try {
137
- const accessToken = localStorage.getItem('access_token');
138
- if (!accessToken) {
139
- throw new Error('Access token not found');
140
- }
141
- await axios.post(`${this.authServer}auth/logout`, {}, {
142
- headers: { Authorization: `Bearer ${accessToken}` }
143
- });
144
- } finally {
145
- localStorage.removeItem('access_token');
146
- localStorage.removeItem('refresh_token');
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
- if (!decodedToken) {
156
- return false;
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
- const { data: publicKey } = await axios.get(`${authServer}public/public_key`);
160
- const { data: algo } = await axios.get(`${authServer}public/algo`);
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
- jwtVerify(bearerToken, publicKey, { algorithms: [algo] });
392
+ if (!decodedToken) {
393
+ throw new Error('Not a valid jwt token');
394
+ }
163
395
 
164
- const { data: revokedIds } = await axios.get(`${authServer}public/revoked_ids`);
165
- return !revokedIds.includes(decodedToken['id']);
166
- } catch (error) {
167
- return false;
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
- public static resetInstance(): void {
172
- AuthManager.instance = null;
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
@@ -1 +1,2 @@
1
+ export * from './types';
1
2
  export { AuthManager } from './AuthManager';
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: 'newAccessToken',
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', 'mockAccessToken');
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('newAccessToken');
64
- expect(localStorage.setItem).toHaveBeenCalledWith('access_token', 'newAccessToken');
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).toHaveBeenCalled();
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
+ });