supaapps-auth 1.4.0 → 2.0.0-rc.1

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,24 @@
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>;
21
+ private saveTokens;
19
22
  loginUsingPkce(code: string): Promise<void>;
20
23
  logout(): Promise<void>;
21
24
  static validateToken(authServer: string, bearerToken: string): Promise<boolean>;
@@ -13,18 +13,30 @@ 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);
27
28
  }
29
+ AuthManager.instance
30
+ .checkAccessToken()
31
+ .then((token) => {
32
+ onStateChange({
33
+ type: types_1.AuthEventType.INITALIZED_IN,
34
+ user: AuthManager.instance.tokenToPayload(token),
35
+ });
36
+ })
37
+ .catch(() => {
38
+ onStateChange({ type: types_1.AuthEventType.INITALIZED_OUT });
39
+ });
28
40
  return AuthManager.instance;
29
41
  }
30
42
  static getInstance() {
@@ -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,37 +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);
75
+ this.saveTokens(response, true);
59
76
  return response.data.access_token;
60
77
  }
61
78
  catch (error) {
62
79
  console.error(`Refresh token error, logging out: ${error}`);
63
80
  localStorage.removeItem('access_token');
64
81
  localStorage.removeItem('refresh_token');
65
- this.loginCallback();
82
+ this.onStateChange({ type: types_1.AuthEventType.REFRESH_FAILED });
66
83
  throw error;
67
84
  }
68
85
  });
69
86
  }
70
87
  checkAccessToken() {
71
88
  return __awaiter(this, void 0, void 0, function* () {
72
- let accessToken = localStorage.getItem('access_token');
73
- if (!accessToken || this.isTokenExpired(accessToken)) {
89
+ const accessToken = localStorage.getItem('access_token');
90
+ if (accessToken || this.isTokenExpired(accessToken)) {
74
91
  return this.refreshAccessToken();
75
92
  }
76
93
  return accessToken;
77
94
  });
78
95
  }
79
96
  isTokenExpired(token) {
80
- const decoded = JSON.parse(atob(token.split('.')[1]));
97
+ const decoded = this.tokenToPayload(token);
81
98
  return decoded.exp < Date.now() / 1000;
82
99
  }
83
100
  mustBeLoggedIn() {
84
101
  return __awaiter(this, void 0, void 0, function* () {
85
- 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
+ }
86
107
  });
87
108
  }
88
109
  getLoginWithGoogleUri() {
@@ -101,9 +122,29 @@ class AuthManager {
101
122
  });
102
123
  }
103
124
  getAccessToken() {
104
- return __awaiter(this, void 0, void 0, function* () {
105
- 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
+ }
137
+ });
138
+ }
139
+ saveTokens(response, byRefresh) {
140
+ localStorage.setItem('access_token', response.data.access_token);
141
+ localStorage.setItem('refresh_token', response.data.refresh_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),
106
145
  });
146
+ const user = this.tokenToPayload(response.data.access_token);
147
+ localStorage.setItem('user', JSON.stringify(user));
107
148
  }
108
149
  loginUsingPkce(code) {
109
150
  return __awaiter(this, void 0, void 0, function* () {
@@ -118,8 +159,7 @@ class AuthManager {
118
159
  redirect_uri: this.redirectUri,
119
160
  code_verifier: codeVerifier,
120
161
  });
121
- localStorage.setItem('access_token', response.data.access_token);
122
- localStorage.setItem('refresh_token', response.data.refresh_token);
162
+ this.saveTokens(response, false);
123
163
  }
124
164
  finally {
125
165
  localStorage.removeItem('codeVerifier');
@@ -135,27 +175,32 @@ class AuthManager {
135
175
  throw new Error('Access token not found');
136
176
  }
137
177
  yield axios_1.default.post(`${this.authServer}auth/logout`, {}, {
138
- headers: { Authorization: `Bearer ${accessToken}` }
178
+ headers: { Authorization: `Bearer ${accessToken}` },
139
179
  });
140
180
  }
141
181
  finally {
142
182
  localStorage.removeItem('access_token');
143
183
  localStorage.removeItem('refresh_token');
184
+ this.onStateChange({ type: types_1.AuthEventType.USER_LOGGED_OUT });
144
185
  }
145
186
  });
146
187
  }
147
188
  static validateToken(authServer, bearerToken) {
148
- var _a;
149
189
  return __awaiter(this, void 0, void 0, function* () {
190
+ var _a;
191
+ // @todo tests missing for this static validation
150
192
  try {
151
- const decodedToken = (_a = jsonwebtoken_1.default.decode(bearerToken, { complete: true })) === null || _a === void 0 ? void 0 : _a.payload;
152
- if (!decodedToken || decodedToken.exp < Date.now() / 1000) {
193
+ const decodedToken = (_a = (0, jsonwebtoken_1.decode)(bearerToken, {
194
+ complete: true,
195
+ })) === null || _a === void 0 ? void 0 : _a.payload;
196
+ if (!decodedToken) {
153
197
  return false;
154
198
  }
155
199
  const { data: publicKey } = yield axios_1.default.get(`${authServer}public/public_key`);
156
200
  const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
157
- jsonwebtoken_1.default.verify(bearerToken, publicKey, { algorithms: [algo] });
201
+ (0, jsonwebtoken_1.verify)(bearerToken, publicKey, { algorithms: [algo] });
158
202
  const { data: revokedIds } = yield axios_1.default.get(`${authServer}public/revoked_ids`);
203
+ // eslint-disable-next-line @typescript-eslint/dot-notation
159
204
  return !revokedIds.includes(decodedToken['id']);
160
205
  }
161
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.0",
3
+ "version": "2.0.0-rc.1",
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,13 +17,26 @@
16
17
  "jsonwebtoken": "^9.0.2"
17
18
  },
18
19
  "devDependencies": {
20
+ "@next/eslint-plugin-next": "^13.5.6",
21
+ "@types/axios": "^0.14.0",
19
22
  "@types/jest": "^29.5.12",
20
- "@types/node": "^20.11.10",
23
+ "@types/jsonwebtoken": "^9.0.6",
24
+ "@types/node": "^20.12.12",
25
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
26
+ "@typescript-eslint/parser": "^6.21.0",
21
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",
22
35
  "jest": "^29.7.0",
23
36
  "jest-localstorage-mock": "^2.4.26",
24
37
  "jest-mock-axios": "^4.7.3",
25
38
  "ts-jest": "^29.1.2",
26
- "typescript": "^5.3.3"
39
+ "typescript": "^5.3.3",
40
+ "undefined": "^0.1.0"
27
41
  }
28
42
  }
@@ -1,166 +1,263 @@
1
- import axios from 'axios';
1
+ import axios, { AxiosResponse } from 'axios';
2
2
  import { createHash, randomBytes } from 'crypto';
3
- import jwt 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
46
  }
47
+ AuthManager.instance
48
+ .checkAccessToken()
49
+ .then((token) => {
50
+ onStateChange({
51
+ type: AuthEventType.INITALIZED_IN,
52
+ user: AuthManager.instance.tokenToPayload(token),
53
+ });
54
+ })
55
+ .catch(() => {
56
+ onStateChange({ type: AuthEventType.INITALIZED_OUT });
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
- return response.data.access_token;
62
- } catch (error) {
63
- console.error(`Refresh token error, logging out: ${error}`);
64
- localStorage.removeItem('access_token');
65
- localStorage.removeItem('refresh_token');
66
- this.loginCallback();
67
- throw error;
68
- }
61
+ public static getInstance(): AuthManager {
62
+ if (!AuthManager.instance) {
63
+ throw new Error('AuthManager not initialized');
69
64
  }
65
+ return AuthManager.instance;
66
+ }
70
67
 
71
- public async checkAccessToken(): Promise<string> {
72
- let accessToken = localStorage.getItem('access_token');
73
- if (!accessToken || this.isTokenExpired(accessToken)) {
74
- return this.refreshAccessToken();
75
- }
76
- return accessToken;
77
- }
68
+ private tokenToPayload(token: string): UserTokenPayload {
69
+ return JSON.parse(atob(token.split('.')[1]));
70
+ }
71
+
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);
78
94
 
79
- private isTokenExpired(token: string): boolean {
80
- const decoded = JSON.parse(atob(token.split('.')[1]));
81
- return decoded.exp < Date.now() / 1000;
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;
82
119
  }
120
+ }
83
121
 
84
- public async mustBeLoggedIn(): Promise<boolean> {
85
- return this.isLoggedIn() || (this.loginCallback(), false);
122
+ public async checkAccessToken(): Promise<string> {
123
+ const accessToken = localStorage.getItem('access_token');
124
+ if (accessToken || this.isTokenExpired(accessToken)) {
125
+ return this.refreshAccessToken();
86
126
  }
127
+ return accessToken;
128
+ }
87
129
 
88
- public getLoginWithGoogleUri(): string {
89
- const { challenge } = this.generatePKCEPair();
90
- return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${challenge}&code_challenge_method=S256`;
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
+ });
91
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
+ }
92
147
 
93
- public async isLoggedIn(): Promise<boolean> {
94
- try {
95
- await this.checkAccessToken();
96
- return true;
97
- } catch (error) {
98
- return false;
99
- }
148
+ public async isLoggedIn(): Promise<boolean> {
149
+ try {
150
+ await this.checkAccessToken();
151
+ return true;
152
+ } catch (error) {
153
+ return false;
100
154
  }
155
+ }
101
156
 
102
- public async getAccessToken(): Promise<string> {
103
- return this.checkAccessToken();
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 '';
104
167
  }
168
+ }
105
169
 
106
- public async loginUsingPkce(code: string): Promise<void> {
107
- try {
108
- const codeVerifier = localStorage.getItem('codeVerifier');
109
- if (!codeVerifier) {
110
- throw new Error('Code verifier not found');
111
- }
112
-
113
- const response = await axios.post(`${this.authServer}auth/pkce_exchange`, {
114
- realm_name: this.realmName,
115
- code,
116
- redirect_uri: this.redirectUri,
117
- code_verifier: codeVerifier,
118
- });
119
-
120
- localStorage.setItem('access_token', response.data.access_token);
121
- localStorage.setItem('refresh_token', response.data.refresh_token);
122
- } finally {
123
- localStorage.removeItem('codeVerifier');
124
- localStorage.removeItem('codeChallenge');
125
- }
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');
126
204
  }
205
+ }
127
206
 
128
- public async logout(): Promise<void> {
129
- try {
130
- const accessToken = localStorage.getItem('access_token');
131
- if (!accessToken) {
132
- throw new Error('Access token not found');
133
- }
134
- await axios.post(`${this.authServer}auth/logout`, {}, {
135
- headers: { Authorization: `Bearer ${accessToken}` }
136
- });
137
- } finally {
138
- localStorage.removeItem('access_token');
139
- localStorage.removeItem('refresh_token');
140
- }
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 });
141
224
  }
225
+ }
142
226
 
143
- public static async validateToken(authServer: string, bearerToken: string): Promise<boolean> {
144
- try {
145
- const decodedToken = jwt.decode(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;
146
236
 
147
- if (!decodedToken || decodedToken.exp < Date.now() / 1000) {
148
- return false;
149
- }
237
+ if (!decodedToken) {
238
+ return false;
239
+ }
150
240
 
151
- const { data: publicKey } = await axios.get(`${authServer}public/public_key`);
152
- 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
+ );
153
247
 
154
- jwt.verify(bearerToken, publicKey, { algorithms: [algo] });
248
+ jwtVerify(bearerToken, publicKey, { algorithms: [algo] });
155
249
 
156
- const { data: revokedIds } = await axios.get(`${authServer}public/revoked_ids`);
157
- return !revokedIds.includes(decodedToken['id']);
158
- } catch (error) {
159
- return false;
160
- }
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;
161
257
  }
258
+ }
162
259
 
163
- public static resetInstance(): void {
164
- AuthManager.instance = null;
165
- }
260
+ public static resetInstance(): void {
261
+ AuthManager.instance = null;
262
+ }
166
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,10 +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';
5
+ import { basename } from 'path';
4
6
 
5
7
  const mock = new MockAdapter(axios);
6
8
 
7
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
+
8
15
  describe('AuthManager Tests', () => {
9
16
 
10
17
  beforeEach(() => {
@@ -44,13 +51,13 @@ describe('AuthManager Tests', () => {
44
51
 
45
52
  it('refreshes access token when expired', async () => {
46
53
  mock.onPost('http://auth-server.com/auth/refresh').reply(200, {
47
- access_token: 'newAccessToken',
54
+ access_token: tokenThatWontExpire2,
48
55
  refresh_token: 'newRefreshToken'
49
56
  });
50
57
 
51
58
  const loginCallback = jest.fn();
52
59
  // check that we set localstorage correct
53
- localStorage.setItem('access_token', 'mockAccessToken');
60
+ localStorage.setItem('access_token', tokenThatExpired);
54
61
  localStorage.setItem('refresh_token', 'mockRefreshToken');
55
62
 
56
63
  const refresh = localStorage.getItem('refresh_token');
@@ -59,8 +66,8 @@ describe('AuthManager Tests', () => {
59
66
  const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
60
67
  const token = await manager.refreshAccessToken();
61
68
 
62
- expect(token).toEqual('newAccessToken');
63
- expect(localStorage.setItem).toHaveBeenCalledWith('access_token', 'newAccessToken');
69
+ expect(token).toEqual(tokenThatWontExpire2);
70
+ expect(localStorage.setItem).toHaveBeenCalledWith('access_token', tokenThatWontExpire2);
64
71
  expect(localStorage.setItem).toHaveBeenCalledWith('refresh_token', 'newRefreshToken');
65
72
  });
66
73
 
@@ -72,13 +79,25 @@ describe('AuthManager Tests', () => {
72
79
  const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
73
80
 
74
81
  await expect(manager.refreshAccessToken()).rejects.toThrow('No refresh token found');
75
- await expect(loginCallback).toHaveBeenCalled();
82
+ await expect(loginCallback).toHaveBeenCalledWith({
83
+ type: AuthEventType.REFRESH_FAILED,
84
+ });
76
85
  });
77
86
 
78
87
  it('logs in using PKCE and updates local storage', async () => {
79
88
  localStorage.setItem('codeVerifier', 'mockCodeVerifier');
89
+ /*
90
+ {
91
+ "sub": "1234567890",
92
+ "name": "John Doe",
93
+ "iat": 1516239022
94
+ }
95
+ */
96
+ const accessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
97
+
98
+
80
99
  mock.onPost('http://auth-server.com/auth/pkce_exchange').reply(200, {
81
- access_token: 'validAccessToken',
100
+ access_token: accessToken,
82
101
  refresh_token: 'validRefreshToken'
83
102
  });
84
103
 
@@ -86,16 +105,18 @@ describe('AuthManager Tests', () => {
86
105
  const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
87
106
  await manager.loginUsingPkce('mockCode');
88
107
 
89
- expect(localStorage.setItem).toHaveBeenCalledWith('access_token', 'validAccessToken');
108
+ expect(localStorage.setItem).toHaveBeenCalledWith('access_token', accessToken);
90
109
  expect(localStorage.setItem).toHaveBeenCalledWith('refresh_token', 'validRefreshToken');
110
+ const userSub = JSON.parse(localStorage.getItem('user') ?? '').sub;
111
+ expect(userSub).toEqual('1234567890');
91
112
  });
92
113
 
93
114
  it('logs out and clears local storage', async () => {
94
- localStorage.setItem('access_token', 'validAccessToken');
95
115
  mock.onPost('http://auth-server.com/auth/logout').reply(200);
96
-
116
+
97
117
  const loginCallback = jest.fn();
98
118
  const manager = AuthManager.initialize('http://auth-server.com/', 'example-realm', 'http://myapp.com/callback', loginCallback);
119
+ localStorage.setItem('access_token', tokenThatWontExpire1);
99
120
  await manager.logout();
100
121
 
101
122
  expect(localStorage.removeItem).toHaveBeenCalledWith('access_token');