supaapps-auth 1.4.1 → 2.0.0-rc.2

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,21 +1,23 @@
1
+ import { AuthManagerEvent } 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
14
  refreshAccessToken(): Promise<string>;
13
15
  checkAccessToken(): 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>;
19
21
  private saveTokens;
20
22
  loginUsingPkce(code: string): Promise<void>;
21
23
  logout(): Promise<void>;
@@ -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()
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;
@@ -52,39 +70,40 @@ class AuthManager {
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
+ this.onStateChange({ type: types_1.AuthEventType.REFRESH_FAILED });
68
83
  throw error;
69
84
  }
70
85
  });
71
86
  }
72
87
  checkAccessToken() {
73
88
  return __awaiter(this, void 0, void 0, function* () {
74
- let accessToken = localStorage.getItem('access_token');
75
- if (!accessToken || this.isTokenExpired(accessToken)) {
89
+ const accessToken = localStorage.getItem('access_token');
90
+ if (accessToken || this.isTokenExpired(accessToken)) {
76
91
  return this.refreshAccessToken();
77
92
  }
78
93
  return accessToken;
79
94
  });
80
95
  }
81
96
  isTokenExpired(token) {
82
- const decoded = JSON.parse(atob(token.split('.')[1]));
97
+ const decoded = this.tokenToPayload(token);
83
98
  return decoded.exp < Date.now() / 1000;
84
99
  }
85
100
  mustBeLoggedIn() {
86
101
  return __awaiter(this, void 0, void 0, function* () {
87
- return this.isLoggedIn() || (this.loginCallback(), false);
102
+ if (!(yield this.isLoggedIn())) {
103
+ this.onStateChange({
104
+ type: types_1.AuthEventType.FAILED_MUST_LOGIN_CHECK,
105
+ });
106
+ }
88
107
  });
89
108
  }
90
109
  getLoginWithGoogleUri() {
@@ -103,14 +122,28 @@ class AuthManager {
103
122
  });
104
123
  }
105
124
  getAccessToken() {
106
- return __awaiter(this, void 0, void 0, function* () {
107
- return this.checkAccessToken();
125
+ return __awaiter(this, arguments, void 0, function* (mustBeLoggedIn = false) {
126
+ try {
127
+ return yield this.checkAccessToken();
128
+ }
129
+ catch (error) {
130
+ if (mustBeLoggedIn) {
131
+ this.onStateChange({
132
+ type: types_1.AuthEventType.FAILED_MUST_LOGIN_CHECK,
133
+ });
134
+ }
135
+ return '';
136
+ }
108
137
  });
109
138
  }
110
- saveTokens(response) {
139
+ saveTokens(response, byRefresh) {
111
140
  localStorage.setItem('access_token', response.data.access_token);
112
141
  localStorage.setItem('refresh_token', response.data.refresh_token);
113
- const user = (0, jsonwebtoken_1.decode)(response.data.access_token);
142
+ this.onStateChange({
143
+ type: byRefresh ? types_1.AuthEventType.USER_UPDATED : types_1.AuthEventType.USER_LOGGED_IN,
144
+ user: this.tokenToPayload(response.data.access_token),
145
+ });
146
+ const user = this.tokenToPayload(response.data.access_token);
114
147
  localStorage.setItem('user', JSON.stringify(user));
115
148
  }
116
149
  loginUsingPkce(code) {
@@ -126,7 +159,7 @@ class AuthManager {
126
159
  redirect_uri: this.redirectUri,
127
160
  code_verifier: codeVerifier,
128
161
  });
129
- this.saveTokens(response);
162
+ this.saveTokens(response, false);
130
163
  }
131
164
  finally {
132
165
  localStorage.removeItem('codeVerifier');
@@ -142,21 +175,24 @@ class AuthManager {
142
175
  throw new Error('Access token not found');
143
176
  }
144
177
  yield axios_1.default.post(`${this.authServer}auth/logout`, {}, {
145
- headers: { Authorization: `Bearer ${accessToken}` }
178
+ headers: { Authorization: `Bearer ${accessToken}` },
146
179
  });
147
180
  }
148
181
  finally {
149
182
  localStorage.removeItem('access_token');
150
183
  localStorage.removeItem('refresh_token');
184
+ this.onStateChange({ type: types_1.AuthEventType.USER_LOGGED_OUT });
151
185
  }
152
186
  });
153
187
  }
154
188
  static validateToken(authServer, bearerToken) {
155
- var _a;
156
189
  return __awaiter(this, void 0, void 0, function* () {
190
+ var _a;
157
191
  // @todo tests missing for this static validation
158
192
  try {
159
- const decodedToken = (_a = (0, jsonwebtoken_1.decode)(bearerToken, { complete: true })) === null || _a === void 0 ? void 0 : _a.payload;
193
+ const decodedToken = (_a = (0, jsonwebtoken_1.decode)(bearerToken, {
194
+ complete: true,
195
+ })) === null || _a === void 0 ? void 0 : _a.payload;
160
196
  if (!decodedToken) {
161
197
  return false;
162
198
  }
@@ -164,6 +200,7 @@ class AuthManager {
164
200
  const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
165
201
  (0, jsonwebtoken_1.verify)(bearerToken, publicKey, { algorithms: [algo] });
166
202
  const { data: revokedIds } = yield axios_1.default.get(`${authServer}public/revoked_ids`);
203
+ // eslint-disable-next-line @typescript-eslint/dot-notation
167
204
  return !revokedIds.includes(decodedToken['id']);
168
205
  }
169
206
  catch (error) {
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,25 @@
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;
14
+ first_name: string;
15
+ last_name: string;
16
+ email: string;
17
+ aud: string;
18
+ iat: number;
19
+ exp: number;
20
+ scopes: string;
21
+ }
22
+ export interface AuthManagerEvent {
23
+ type: AuthEventType;
24
+ user?: UserTokenPayload;
25
+ }
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ 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 = {}));
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.2",
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.0",
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,263 @@
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, 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;
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;
18
- }
10
+ private static instance: AuthManager | null = null;
19
11
 
20
- public static initialize(authServer: string, realmName: string, redirectUri: string, loginCallback: () => void): AuthManager {
21
- if (!AuthManager.instance) {
22
- AuthManager.instance = new AuthManager(authServer, realmName, redirectUri, loginCallback);
23
- }
24
- return AuthManager.instance;
25
- }
12
+ private authServer: string;
26
13
 
27
- public static getInstance(): AuthManager {
28
- if (!AuthManager.instance) {
29
- throw new Error('AuthManager not initialized');
30
- }
31
- return AuthManager.instance;
32
- }
14
+ private realmName: string;
33
15
 
34
- private toBase64Url(base64String: string): string {
35
- return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
36
- }
16
+ private redirectUri: string;
37
17
 
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'));
18
+ private onStateChange: (event: AuthManagerEvent) => void;
41
19
 
42
- localStorage.setItem('codeVerifier', verifier);
43
- localStorage.setItem('codeChallenge', challenge);
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
+ }
44
32
 
45
- return { verifier, challenge };
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()
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
+ });
46
57
  }
58
+ return AuthManager.instance;
59
+ }
47
60
 
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
- }
54
-
55
- const response = await axios.post(`${this.authServer}auth/refresh`, {
56
- refresh_token: refreshToken
57
- });
58
-
59
- localStorage.setItem('refresh_token', response.data.refresh_token);
60
- localStorage.setItem('access_token', response.data.access_token);
61
- 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
- }
61
+ public static getInstance(): AuthManager {
62
+ if (!AuthManager.instance) {
63
+ throw new Error('AuthManager not initialized');
71
64
  }
65
+ return AuthManager.instance;
66
+ }
72
67
 
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;
79
- }
68
+ private tokenToPayload(token: string): UserTokenPayload {
69
+ return JSON.parse(atob(token.split('.')[1]));
70
+ }
80
71
 
81
- private isTokenExpired(token: string): boolean {
82
- const decoded = JSON.parse(atob(token.split('.')[1]));
83
- return decoded.exp < Date.now() / 1000;
84
- }
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);
85
94
 
86
- public async mustBeLoggedIn(): Promise<boolean> {
87
- return this.isLoggedIn() || (this.loginCallback(), false);
95
+ return { verifier, challenge };
96
+ }
97
+
98
+ public async refreshAccessToken(): 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
+ this.onStateChange({ type: AuthEventType.REFRESH_FAILED });
118
+ throw error;
88
119
  }
120
+ }
89
121
 
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`;
122
+ public async checkAccessToken(): Promise<string> {
123
+ const accessToken = localStorage.getItem('access_token');
124
+ if (accessToken || this.isTokenExpired(accessToken)) {
125
+ return this.refreshAccessToken();
93
126
  }
127
+ return accessToken;
128
+ }
94
129
 
95
- public async isLoggedIn(): Promise<boolean> {
96
- try {
97
- await this.checkAccessToken();
98
- return true;
99
- } catch (error) {
100
- return false;
101
- }
130
+ private isTokenExpired(token: string): boolean {
131
+ const decoded = this.tokenToPayload(token);
132
+ return decoded.exp < Date.now() / 1000;
133
+ }
134
+
135
+ public async mustBeLoggedIn(): Promise<void> {
136
+ if (!(await this.isLoggedIn())) {
137
+ this.onStateChange({
138
+ type: AuthEventType.FAILED_MUST_LOGIN_CHECK,
139
+ });
102
140
  }
141
+ }
142
+
143
+ public getLoginWithGoogleUri(): string {
144
+ const { challenge } = this.generatePKCEPair();
145
+ return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256`;
146
+ }
103
147
 
104
- public async getAccessToken(): Promise<string> {
105
- return this.checkAccessToken();
148
+ public async isLoggedIn(): Promise<boolean> {
149
+ try {
150
+ await this.checkAccessToken();
151
+ return true;
152
+ } catch (error) {
153
+ return false;
106
154
  }
155
+ }
107
156
 
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));
157
+ public async getAccessToken(mustBeLoggedIn: boolean = false): Promise<string> {
158
+ try {
159
+ return await this.checkAccessToken();
160
+ } catch (error) {
161
+ if (mustBeLoggedIn) {
162
+ this.onStateChange({
163
+ type: AuthEventType.FAILED_MUST_LOGIN_CHECK,
164
+ });
165
+ }
166
+ return '';
113
167
  }
168
+ }
114
169
 
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
- }
121
-
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
- }
170
+ private saveTokens(response: AxiosResponse, byRefresh: boolean): void {
171
+ localStorage.setItem('access_token', response.data.access_token);
172
+ localStorage.setItem(
173
+ 'refresh_token',
174
+ response.data.refresh_token,
175
+ );
176
+ this.onStateChange({
177
+ type: byRefresh ? AuthEventType.USER_UPDATED : AuthEventType.USER_LOGGED_IN,
178
+ user: this.tokenToPayload(response.data.access_token),
179
+ });
180
+ const user = this.tokenToPayload(response.data.access_token);
181
+ localStorage.setItem('user', JSON.stringify(user));
182
+ }
183
+
184
+ public async loginUsingPkce(code: string): Promise<void> {
185
+ try {
186
+ const codeVerifier = localStorage.getItem('codeVerifier');
187
+ if (!codeVerifier) {
188
+ throw new Error('Code verifier not found');
189
+ }
190
+
191
+ const response = await axios.post(
192
+ `${this.authServer}auth/pkce_exchange`,
193
+ {
194
+ realm_name: this.realmName,
195
+ code,
196
+ redirect_uri: this.redirectUri,
197
+ code_verifier: codeVerifier,
198
+ },
199
+ );
200
+ this.saveTokens(response, false);
201
+ } finally {
202
+ localStorage.removeItem('codeVerifier');
203
+ localStorage.removeItem('codeChallenge');
133
204
  }
205
+ }
134
206
 
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
- }
207
+ public async logout(): Promise<void> {
208
+ try {
209
+ const accessToken = localStorage.getItem('access_token');
210
+ if (!accessToken) {
211
+ throw new Error('Access token not found');
212
+ }
213
+ await axios.post(
214
+ `${this.authServer}auth/logout`,
215
+ {},
216
+ {
217
+ headers: { Authorization: `Bearer ${accessToken}` },
218
+ },
219
+ );
220
+ } finally {
221
+ localStorage.removeItem('access_token');
222
+ localStorage.removeItem('refresh_token');
223
+ this.onStateChange({ type: AuthEventType.USER_LOGGED_OUT });
148
224
  }
225
+ }
149
226
 
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;
227
+ public static async validateToken(
228
+ authServer: string,
229
+ bearerToken: string,
230
+ ): Promise<boolean> {
231
+ // @todo tests missing for this static validation
232
+ try {
233
+ const decodedToken = jwtDecode(bearerToken, {
234
+ complete: true,
235
+ })?.payload;
154
236
 
155
- if (!decodedToken) {
156
- return false;
157
- }
237
+ if (!decodedToken) {
238
+ return false;
239
+ }
158
240
 
159
- const { data: publicKey } = await axios.get(`${authServer}public/public_key`);
160
- const { data: algo } = await axios.get(`${authServer}public/algo`);
241
+ const { data: publicKey } = await axios.get(
242
+ `${authServer}public/public_key`,
243
+ );
244
+ const { data: algo } = await axios.get(
245
+ `${authServer}public/algo`,
246
+ );
161
247
 
162
- jwtVerify(bearerToken, publicKey, { algorithms: [algo] });
248
+ jwtVerify(bearerToken, publicKey, { algorithms: [algo] });
163
249
 
164
- const { data: revokedIds } = await axios.get(`${authServer}public/revoked_ids`);
165
- return !revokedIds.includes(decodedToken['id']);
166
- } catch (error) {
167
- return false;
168
- }
250
+ const { data: revokedIds } = await axios.get(
251
+ `${authServer}public/revoked_ids`,
252
+ );
253
+ // eslint-disable-next-line @typescript-eslint/dot-notation
254
+ return !revokedIds.includes(decodedToken['id']);
255
+ } catch (error) {
256
+ return false;
169
257
  }
258
+ }
170
259
 
171
- public static resetInstance(): void {
172
- AuthManager.instance = null;
173
- }
260
+ public static resetInstance(): void {
261
+ AuthManager.instance = null;
262
+ }
174
263
  }
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,28 @@
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;
16
+ first_name: string;
17
+ last_name: string;
18
+ email: string;
19
+ aud: string;
20
+ iat: number;
21
+ exp: number;
22
+ scopes: string;
23
+ }
24
+
25
+ export interface AuthManagerEvent {
26
+ type: AuthEventType;
27
+ user?: UserTokenPayload;
28
+ }
@@ -1,11 +1,17 @@
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
+
9
15
  describe('AuthManager Tests', () => {
10
16
 
11
17
  beforeEach(() => {
@@ -45,13 +51,13 @@ describe('AuthManager Tests', () => {
45
51
 
46
52
  it('refreshes access token when expired', async () => {
47
53
  mock.onPost('http://auth-server.com/auth/refresh').reply(200, {
48
- access_token: 'newAccessToken',
54
+ access_token: tokenThatWontExpire2,
49
55
  refresh_token: 'newRefreshToken'
50
56
  });
51
57
 
52
58
  const loginCallback = jest.fn();
53
59
  // check that we set localstorage correct
54
- localStorage.setItem('access_token', 'mockAccessToken');
60
+ localStorage.setItem('access_token', tokenThatExpired);
55
61
  localStorage.setItem('refresh_token', 'mockRefreshToken');
56
62
 
57
63
  const refresh = localStorage.getItem('refresh_token');
@@ -60,8 +66,8 @@ describe('AuthManager Tests', () => {
60
66
  const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
61
67
  const token = await manager.refreshAccessToken();
62
68
 
63
- expect(token).toEqual('newAccessToken');
64
- expect(localStorage.setItem).toHaveBeenCalledWith('access_token', 'newAccessToken');
69
+ expect(token).toEqual(tokenThatWontExpire2);
70
+ expect(localStorage.setItem).toHaveBeenCalledWith('access_token', tokenThatWontExpire2);
65
71
  expect(localStorage.setItem).toHaveBeenCalledWith('refresh_token', 'newRefreshToken');
66
72
  });
67
73
 
@@ -73,7 +79,9 @@ describe('AuthManager Tests', () => {
73
79
  const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
74
80
 
75
81
  await expect(manager.refreshAccessToken()).rejects.toThrow('No refresh token found');
76
- await expect(loginCallback).toHaveBeenCalled();
82
+ await expect(loginCallback).toHaveBeenCalledWith({
83
+ type: AuthEventType.REFRESH_FAILED,
84
+ });
77
85
  });
78
86
 
79
87
  it('logs in using PKCE and updates local storage', async () => {
@@ -104,11 +112,11 @@ describe('AuthManager Tests', () => {
104
112
  });
105
113
 
106
114
  it('logs out and clears local storage', async () => {
107
- localStorage.setItem('access_token', 'validAccessToken');
108
115
  mock.onPost('http://auth-server.com/auth/logout').reply(200);
109
-
116
+
110
117
  const loginCallback = jest.fn();
111
118
  const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
119
+ localStorage.setItem('access_token', tokenThatWontExpire1);
112
120
  await manager.logout();
113
121
 
114
122
  expect(localStorage.removeItem).toHaveBeenCalledWith('access_token');