supaapps-auth 1.0.6 → 1.2.0

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/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # TODO
2
+
3
+
4
+ `npm i supaapps-auth`
5
+
6
+
7
+ - Initialize
8
+ ```ts
9
+ new AuthManager(
10
+ 'https://supaapps-auth-api.testing.sacl.io/',
11
+ 'root',
12
+ 'http://localhost:3001/exchange',
13
+ () => {
14
+ // redirect to login
15
+ }
16
+ );
17
+ ```
18
+
19
+
20
+ - Require login
21
+
22
+ ```ts
23
+ import {AuthManager} from "./AuthManager";
24
+
25
+ const authManager = AuthManager.getInstance();
26
+ authManager.mustBeLoggedIn().then((isLoggedIn) => {
27
+ if (isLoggedIn) {
28
+ // do something
29
+ }
30
+ });
31
+
32
+ // or
33
+
34
+ AuthManager.getInstance().mustBeLoggedIn();
35
+ ```
36
+
37
+
38
+ - Get user info
39
+
40
+ ```ts
41
+ authManager.mustBeLoggedIn().then(
42
+ (isLoggedIn) => {
43
+ isLoggedIn && authManager.getAccessToken().then(
44
+ (token) => {
45
+ const decodedToken = JSON.parse(atob(token.split('.')[1]));
46
+ // access info for example
47
+ // decodedToken.first_name
48
+ }
49
+ );
50
+ }
51
+ )
52
+ ```
53
+
54
+
55
+
56
+ Get login uri
57
+
58
+ ```ts
59
+ AuthManager.getInstance().getLoginWithGoogleUri()
60
+ ```
61
+
62
+
63
+ - Log user out
64
+
65
+ ```ts
66
+ await authManager.logout();
67
+ // or
68
+ authManager.logout().then(() => {
69
+ // user is now logged out
70
+ })
71
+ ```
72
+
73
+ - Validate access token
74
+
75
+ ```typescript
76
+ import { AuthManager } from './AuthManager';
77
+
78
+ const isValid = await AuthManager.validateToken(BEARER_HEADER_OR_ACCESS_TOKEN)
79
+ // or
80
+ AuthManager.validateToken(BEARER_OR_ACCESS_TOKEN).then((isValid) => {
81
+ if (isValid) {
82
+ // token is valid
83
+ }
84
+ })
85
+ ```
@@ -8,9 +8,13 @@ export declare class AuthManager {
8
8
  static getInstance<T>(): AuthManager;
9
9
  private toBase64Url;
10
10
  private generatePKCEPair;
11
+ private refreshAccessToken;
12
+ private checkAccessToken;
11
13
  mustBeLoggedIn(): Promise<boolean>;
12
14
  getLoginWithGoogleUri(): string;
13
15
  isLoggedIn(): Promise<boolean>;
14
16
  getAccessToken(): Promise<string>;
15
17
  loginUsingPkce(code: any): Promise<void>;
18
+ logout(): Promise<void>;
19
+ static validateToken(authServer: string, bearerToken: string): Promise<boolean>;
16
20
  }
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.AuthManager = void 0;
13
+ const axios_1 = require("axios");
13
14
  const crypto_1 = require("crypto");
14
15
  class AuthManager {
15
16
  constructor(authServer, realmName, redirectUri, loginCallback) {
@@ -47,6 +48,79 @@ class AuthManager {
47
48
  }
48
49
  return AuthManager.instance;
49
50
  }
51
+ refreshAccessToken() {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
54
+ try {
55
+ const refreshToken = localStorage.getItem('refresh_token');
56
+ if (!refreshToken) {
57
+ throw new Error('No refresh token found');
58
+ }
59
+ const decodedRefreshToken = JSON.parse(atob(refreshToken.split('.')[1]));
60
+ if (decodedRefreshToken) {
61
+ const currentTime = Date.now() / 1000;
62
+ if (decodedRefreshToken.exp < currentTime) {
63
+ throw new Error('Refresh token expired');
64
+ }
65
+ }
66
+ yield fetch(`${this.authServer}auth/refresh`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ },
71
+ body: JSON.stringify({
72
+ refresh_token: refreshToken,
73
+ }),
74
+ })
75
+ .then((response) => {
76
+ if (response.status !== 200) {
77
+ throw new Error('Failed to refresh the token');
78
+ }
79
+ return response.json();
80
+ })
81
+ .then((exchangeJson) => {
82
+ localStorage.setItem('refresh_token', exchangeJson.refresh_token);
83
+ localStorage.setItem('access_token', exchangeJson.access_token);
84
+ resolve(exchangeJson.access_token);
85
+ })
86
+ .catch((error) => {
87
+ reject(error);
88
+ });
89
+ }
90
+ catch (error) {
91
+ reject(error);
92
+ }
93
+ }));
94
+ });
95
+ }
96
+ checkAccessToken() {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
99
+ try {
100
+ const accessToken = localStorage.getItem('access_token');
101
+ if (!accessToken) {
102
+ throw new Error('No access token found');
103
+ }
104
+ // decode access token and check if it's expired
105
+ const decodedToken = accessToken
106
+ ? JSON.parse(atob(accessToken.split('.')[1]))
107
+ : null;
108
+ if (decodedToken) {
109
+ const currentTime = Date.now() / 1000;
110
+ if (decodedToken.exp < currentTime) {
111
+ // refreshing expired token
112
+ const newAccessToken = yield this.refreshAccessToken();
113
+ return resolve(newAccessToken);
114
+ }
115
+ }
116
+ resolve(accessToken);
117
+ }
118
+ catch (error) {
119
+ reject(error);
120
+ }
121
+ }));
122
+ });
123
+ }
50
124
  mustBeLoggedIn() {
51
125
  return __awaiter(this, void 0, void 0, function* () {
52
126
  return new Promise((resolve, reject) => {
@@ -75,56 +149,30 @@ class AuthManager {
75
149
  isLoggedIn() {
76
150
  return __awaiter(this, void 0, void 0, function* () {
77
151
  // todo here: check if refresh token is expired and if so, try to refresh, then update token
78
- return new Promise((resolve, reject) => {
152
+ return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
79
153
  try {
80
- const accessToken = localStorage.getItem('access_token');
81
- if (!accessToken) {
82
- return resolve(false);
83
- }
84
- // decode access token and check if it's expired
85
- const decodedToken = accessToken ? JSON.parse(atob(accessToken.split('.')[1])) : null;
86
- if (decodedToken) {
87
- const currentTime = Date.now() / 1000;
88
- if (decodedToken.exp < currentTime) {
89
- // add refresh check here instead and
90
- localStorage.removeItem('access_token');
91
- return resolve(false);
92
- }
93
- }
154
+ yield this.checkAccessToken();
94
155
  return resolve(true);
95
156
  }
96
157
  catch (error) {
97
158
  reject(error);
98
159
  }
99
- });
160
+ }));
100
161
  });
101
162
  }
102
163
  getAccessToken() {
103
164
  return __awaiter(this, void 0, void 0, function* () {
104
165
  // todo here: check if refresh token is expired and if so, try to refresh, then update token
105
166
  // otherwise throw error
106
- return new Promise((resolve, reject) => {
167
+ return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
107
168
  try {
108
- const accessToken = localStorage.getItem('access_token');
109
- if (!accessToken) {
110
- throw new Error('No access token found');
111
- }
112
- // decode access token and check if it's expired
113
- const decodedToken = accessToken ? JSON.parse(atob(accessToken.split('.')[1])) : null;
114
- if (decodedToken) {
115
- const currentTime = Date.now() / 1000;
116
- if (decodedToken.exp < currentTime) {
117
- // add refresh check here instead and
118
- localStorage.removeItem('access_token');
119
- throw new Error('Access token expired');
120
- }
121
- }
169
+ const accessToken = yield this.checkAccessToken();
122
170
  return resolve(accessToken);
123
171
  }
124
172
  catch (error) {
125
173
  reject(error);
126
174
  }
127
- });
175
+ }));
128
176
  });
129
177
  }
130
178
  loginUsingPkce(code) {
@@ -171,6 +219,68 @@ class AuthManager {
171
219
  });
172
220
  });
173
221
  }
222
+ logout() {
223
+ return __awaiter(this, void 0, void 0, function* () {
224
+ return new Promise((resolve, reject) => {
225
+ try {
226
+ const bearerToken = localStorage.getItem('access_token');
227
+ localStorage.removeItem('access_token');
228
+ localStorage.removeItem('refresh_token');
229
+ fetch(`${this.authServer}auth/logout`, {
230
+ method: 'POST',
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ 'Authorization': `Bearer ${bearerToken}`,
234
+ },
235
+ }).then((response) => {
236
+ if (response.status !== 200) {
237
+ throw new Error('Failed to attempt logout');
238
+ }
239
+ resolve();
240
+ }).catch((error) => {
241
+ reject(error);
242
+ });
243
+ }
244
+ catch (error) {
245
+ reject(error);
246
+ }
247
+ });
248
+ });
249
+ }
250
+ static validateToken(authServer, bearerToken) {
251
+ return __awaiter(this, void 0, void 0, function* () {
252
+ return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
253
+ try {
254
+ const accessToken = bearerToken.includes('Bearer ') ? bearerToken.replace('Bearer ', '') : bearerToken;
255
+ const decodedToken = accessToken ? JSON.parse(atob(accessToken.split('.')[1])) : null;
256
+ if (!decodedToken) {
257
+ return resolve(false);
258
+ }
259
+ const currentTime = Date.now() / 1000;
260
+ if (decodedToken.exp < currentTime) {
261
+ return resolve(false);
262
+ }
263
+ const { data: publicKey } = yield axios_1.default.get(`${authServer}public/public_key`);
264
+ const { data: algo } = yield axios_1.default.get(`${authServer}public/algo`);
265
+ const jwt = require('jsonwebtoken');
266
+ jwt.verify(accessToken, publicKey, { algorithms: [algo] }, (error, payload) => {
267
+ if (error) {
268
+ return resolve(false);
269
+ }
270
+ axios_1.default.get(`${authServer}public/revoked_ids`).then(({ data: revokedIds }) => {
271
+ if (revokedIds && revokedIds.includes(decodedToken['id'])) {
272
+ return resolve(false);
273
+ }
274
+ return resolve(true);
275
+ });
276
+ });
277
+ }
278
+ catch (error) {
279
+ reject(error);
280
+ }
281
+ }));
282
+ });
283
+ }
174
284
  }
175
285
  exports.AuthManager = AuthManager;
176
286
  AuthManager.instance = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supaapps-auth",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  "license": "MIT",
13
13
  "dependencies": {
14
14
  "axios": "^1.6.7",
15
- "crypto": "^1.0.1"
15
+ "crypto": "^1.0.1",
16
+ "jsonwebtoken": "^9.0.2"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@types/node": "^20.11.10",
@@ -1,7 +1,6 @@
1
1
  import axios from 'axios';
2
2
  import { createHash, randomBytes } from 'crypto';
3
3
 
4
-
5
4
  export class AuthManager {
6
5
  private static instance: AuthManager | null = null;
7
6
  private readonly authServer: string | null = null;
@@ -28,9 +27,9 @@ export class AuthManager {
28
27
 
29
28
  private toBase64Url = (base64String: string) => {
30
29
  return base64String
31
- .replace(/\+/g, '-')
32
- .replace(/\//g, '_')
33
- .replace(/=+$/, '');
30
+ .replace(/\+/g, '-')
31
+ .replace(/\//g, '_')
32
+ .replace(/=+$/, '');
34
33
  };
35
34
  private generatePKCEPair = () => {
36
35
  const NUM_OF_BYTES = 32; // This will generate a verifier of sufficient length
@@ -38,18 +37,86 @@ export class AuthManager {
38
37
 
39
38
  // Generate code verifier
40
39
  const newCodeVerifier = this.toBase64Url(
41
- randomBytes(NUM_OF_BYTES).toString('base64'),
40
+ randomBytes(NUM_OF_BYTES).toString('base64'),
42
41
  );
43
42
 
44
43
  // Generate code challenge
45
44
  const hash = createHash(HASH_ALG)
46
- .update(newCodeVerifier)
47
- .digest('base64');
45
+ .update(newCodeVerifier)
46
+ .digest('base64');
48
47
  const newCodeChallenge = this.toBase64Url(hash);
49
48
 
50
49
  return { newCodeVerifier, newCodeChallenge };
51
50
  };
52
51
 
52
+ private async refreshAccessToken(): Promise<string> {
53
+ return new Promise(async (resolve, reject) => {
54
+ try {
55
+ const refreshToken: string | null = localStorage.getItem('refresh_token');
56
+ if (!refreshToken) {
57
+ throw new Error('No refresh token found');
58
+ }
59
+ const decodedRefreshToken = JSON.parse(atob(refreshToken.split('.')[1]));
60
+ if (decodedRefreshToken) {
61
+ const currentTime = Date.now() / 1000;
62
+ if (decodedRefreshToken.exp < currentTime) {
63
+ throw new Error('Refresh token expired');
64
+ }
65
+ }
66
+ await fetch(`${this.authServer}auth/refresh`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ },
71
+ body: JSON.stringify({
72
+ refresh_token: refreshToken,
73
+ }),
74
+ })
75
+ .then((response) => {
76
+ if (response.status !== 200) {
77
+ throw new Error('Failed to refresh the token');
78
+ }
79
+ return response.json();
80
+ })
81
+ .then((exchangeJson) => {
82
+ localStorage.setItem('refresh_token', exchangeJson.refresh_token);
83
+ localStorage.setItem('access_token', exchangeJson.access_token);
84
+ resolve(exchangeJson.access_token);
85
+ })
86
+ .catch((error) => {
87
+ reject(error);
88
+ });
89
+ } catch (error) {
90
+ reject(error);
91
+ }
92
+ });
93
+ }
94
+ private async checkAccessToken(): Promise<string> {
95
+ return new Promise(async (resolve, reject) => {
96
+ try {
97
+ const accessToken: string | null = localStorage.getItem('access_token');
98
+ if (!accessToken) {
99
+ throw new Error('No access token found');
100
+ }
101
+ // decode access token and check if it's expired
102
+ const decodedToken = accessToken
103
+ ? JSON.parse(atob(accessToken.split('.')[1]))
104
+ : null;
105
+ if (decodedToken) {
106
+ const currentTime = Date.now() / 1000;
107
+ if (decodedToken.exp < currentTime) {
108
+ // refreshing expired token
109
+ const newAccessToken = await this.refreshAccessToken();
110
+ return resolve(newAccessToken);
111
+ }
112
+ }
113
+ resolve(accessToken);
114
+ } catch (error) {
115
+ reject(error);
116
+ }
117
+ })
118
+ }
119
+
53
120
  public async mustBeLoggedIn(): Promise<boolean> {
54
121
  return new Promise((resolve, reject) => {
55
122
  this.isLoggedIn().then((isLoggedIn) => {
@@ -72,28 +139,15 @@ export class AuthManager {
72
139
 
73
140
  if (this.authServer && this.realmName && this.redirectUri) {
74
141
  return `${this.authServer}auth/login_with_google?realm_name=${this.realmName}` +
75
- `&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256`
142
+ `&redirect_uri=${encodeURIComponent(this.redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256`
76
143
  }
77
144
  }
145
+
78
146
  public async isLoggedIn(): Promise<boolean> {
79
147
  // todo here: check if refresh token is expired and if so, try to refresh, then update token
80
- return new Promise((resolve, reject) => {
148
+ return new Promise(async (resolve, reject) => {
81
149
  try {
82
- const accessToken: string | null = localStorage.getItem('access_token');
83
- if (!accessToken) {
84
- return resolve(false);
85
- }
86
- // decode access token and check if it's expired
87
- const decodedToken = accessToken ? JSON.parse(atob(accessToken.split('.')[1])) : null;
88
- if (decodedToken) {
89
- const currentTime = Date.now() / 1000;
90
- if (decodedToken.exp < currentTime) {
91
- // add refresh check here instead and
92
- localStorage.removeItem('access_token');
93
- return resolve(false);
94
- }
95
- }
96
-
150
+ await this.checkAccessToken();
97
151
  return resolve(true);
98
152
  } catch (error) {
99
153
  reject(error);
@@ -104,22 +158,9 @@ export class AuthManager {
104
158
  public async getAccessToken(): Promise<string> {
105
159
  // todo here: check if refresh token is expired and if so, try to refresh, then update token
106
160
  // otherwise throw error
107
- return new Promise((resolve, reject) => {
161
+ return new Promise(async (resolve, reject) => {
108
162
  try {
109
- const accessToken: string | null = localStorage.getItem('access_token');
110
- if (!accessToken) {
111
- throw new Error('No access token found');
112
- }
113
- // decode access token and check if it's expired
114
- const decodedToken = accessToken ? JSON.parse(atob(accessToken.split('.')[1])) : null;
115
- if (decodedToken) {
116
- const currentTime = Date.now() / 1000;
117
- if (decodedToken.exp < currentTime) {
118
- // add refresh check here instead and
119
- localStorage.removeItem('access_token');
120
- throw new Error('Access token expired');
121
- }
122
- }
163
+ const accessToken = await this.checkAccessToken();
123
164
 
124
165
  return resolve(accessToken);
125
166
  } catch (error) {
@@ -145,24 +186,24 @@ export class AuthManager {
145
186
  code_verifier: codeVerifier,
146
187
  }),
147
188
  })
148
- .then((response) => {
149
- localStorage.removeItem('codeVerifier');
150
- localStorage.removeItem('codeChallenge');
151
- if (response.status !== 200) {
152
- throw new Error('Failed to exchange code for token');
153
- }
154
- return response.json();
155
- })
156
- .then((exchangeJson) => {
157
- localStorage.setItem('access_token', exchangeJson.access_token);
158
- localStorage.setItem('refresh_token', exchangeJson.refresh_token);
159
- resolve();
160
- })
161
- .catch((error) => {
162
- localStorage.removeItem('codeVerifier');
163
- localStorage.removeItem('codeChallenge');
164
- reject(error);
165
- });
189
+ .then((response) => {
190
+ localStorage.removeItem('codeVerifier');
191
+ localStorage.removeItem('codeChallenge');
192
+ if (response.status !== 200) {
193
+ throw new Error('Failed to exchange code for token');
194
+ }
195
+ return response.json();
196
+ })
197
+ .then((exchangeJson) => {
198
+ localStorage.setItem('access_token', exchangeJson.access_token);
199
+ localStorage.setItem('refresh_token', exchangeJson.refresh_token);
200
+ resolve();
201
+ })
202
+ .catch((error) => {
203
+ localStorage.removeItem('codeVerifier');
204
+ localStorage.removeItem('codeChallenge');
205
+ reject(error);
206
+ });
166
207
  }
167
208
  } catch (error) {
168
209
  reject(error);
@@ -170,4 +211,64 @@ export class AuthManager {
170
211
  });
171
212
  }
172
213
 
214
+ public async logout(): Promise<void> {
215
+ return new Promise((resolve, reject) => {
216
+ try {
217
+ const bearerToken = localStorage.getItem('access_token');
218
+ localStorage.removeItem('access_token');
219
+ localStorage.removeItem('refresh_token');
220
+ fetch(`${this.authServer}auth/logout`, {
221
+ method: 'POST',
222
+ headers: {
223
+ 'Content-Type': 'application/json',
224
+ 'Authorization': `Bearer ${bearerToken}`,
225
+ },
226
+ }).then((response) => {
227
+ if (response.status !== 200) {
228
+ throw new Error('Failed to attempt logout')
229
+ }
230
+ resolve();
231
+ }).catch((error) => {
232
+ reject(error);
233
+ })
234
+ } catch (error) {
235
+ reject(error);
236
+ }
237
+ })
238
+ }
239
+
240
+ public static async validateToken(authServer: string, bearerToken: string): Promise<boolean> {
241
+ return new Promise<boolean>(async (resolve, reject) => {
242
+ try {
243
+ const accessToken = bearerToken.includes('Bearer ') ? bearerToken.replace('Bearer ', '') : bearerToken;
244
+ const decodedToken = accessToken ? JSON.parse(atob(accessToken.split('.')[1])) : null;
245
+
246
+ if (!decodedToken) {
247
+ return resolve(false);
248
+ }
249
+
250
+ const currentTime = Date.now() / 1000;
251
+ if (decodedToken.exp < currentTime) {
252
+ return resolve(false);
253
+ }
254
+
255
+ const { data: publicKey } = await axios.get(`${authServer}public/public_key`);
256
+ const { data: algo } = await axios.get(`${authServer}public/algo`);
257
+ const jwt = require('jsonwebtoken');
258
+ jwt.verify(accessToken, publicKey, { algorithms: [algo] }, (error, payload) => {
259
+ if (error) {
260
+ return resolve(false);
261
+ }
262
+ axios.get(`${authServer}public/revoked_ids`).then(({ data: revokedIds }) => {
263
+ if (revokedIds && (revokedIds as number[]).includes(decodedToken['id'])) {
264
+ return resolve(false);
265
+ }
266
+ return resolve(true);
267
+ });
268
+ });
269
+ } catch (error) {
270
+ reject(error);
271
+ }
272
+ })
273
+ }
173
274
  }