@descope/angular-sdk 0.5.9 → 0.5.11

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.
Files changed (150) hide show
  1. package/.eslintrc.json +41 -0
  2. package/.prettierrc +7 -0
  3. package/CHANGELOG.md +5 -0
  4. package/LICENSE +21 -0
  5. package/README.md +647 -11
  6. package/angular.json +154 -0
  7. package/{esm2022 → dist/esm2022}/environment.mjs +2 -2
  8. package/dist/esm2022/lib/components/access-key-management/access-key-management.component.mjs +61 -0
  9. package/dist/esm2022/lib/components/audit-management/audit-management.component.mjs +61 -0
  10. package/dist/esm2022/lib/components/descope/descope.component.mjs +156 -0
  11. package/dist/esm2022/lib/components/role-management/role-management.component.mjs +61 -0
  12. package/dist/esm2022/lib/components/sign-in-flow/sign-in-flow.component.mjs +44 -0
  13. package/dist/esm2022/lib/components/sign-up-flow/sign-up-flow.component.mjs +44 -0
  14. package/dist/esm2022/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.mjs +44 -0
  15. package/dist/esm2022/lib/components/user-management/user-management.component.mjs +61 -0
  16. package/dist/esm2022/lib/components/user-profile/user-profile.component.mjs +66 -0
  17. package/{esm2022 → dist/esm2022}/lib/descope-auth.module.mjs +5 -5
  18. package/{esm2022 → dist/esm2022}/lib/services/descope-auth.guard.mjs +1 -1
  19. package/dist/esm2022/lib/services/descope-auth.service.mjs +154 -0
  20. package/dist/esm2022/lib/services/descope.interceptor.mjs +51 -0
  21. package/dist/esm2022/lib/types/types.mjs +6 -0
  22. package/{esm2022 → dist/esm2022}/lib/utils/constants.mjs +1 -1
  23. package/dist/esm2022/lib/utils/helpers.mjs +27 -0
  24. package/{fesm2022 → dist/fesm2022}/descope-angular-sdk.mjs +62 -38
  25. package/dist/fesm2022/descope-angular-sdk.mjs.map +1 -0
  26. package/{lib → dist/lib}/components/access-key-management/access-key-management.component.d.ts +1 -0
  27. package/{lib → dist/lib}/components/audit-management/audit-management.component.d.ts +1 -0
  28. package/{lib → dist/lib}/components/descope/descope.component.d.ts +1 -0
  29. package/{lib → dist/lib}/components/role-management/role-management.component.d.ts +1 -0
  30. package/{lib → dist/lib}/components/user-management/user-management.component.d.ts +1 -0
  31. package/{lib → dist/lib}/components/user-profile/user-profile.component.d.ts +1 -0
  32. package/{lib → dist/lib}/services/descope-auth.service.d.ts +3 -2
  33. package/{lib → dist/lib}/types/types.d.ts +1 -0
  34. package/jest.config.js +17 -0
  35. package/package.json +62 -19
  36. package/project.json +17 -0
  37. package/projects/angular-sdk/.eslintrc.json +32 -0
  38. package/projects/angular-sdk/ng-package.json +17 -0
  39. package/projects/angular-sdk/package.json +87 -0
  40. package/projects/angular-sdk/src/environment.ts +3 -0
  41. package/projects/angular-sdk/src/lib/components/access-key-management/access-key-management.component.spec.ts +84 -0
  42. package/projects/angular-sdk/src/lib/components/access-key-management/access-key-management.component.ts +63 -0
  43. package/projects/angular-sdk/src/lib/components/audit-management/audit-management.component.spec.ts +84 -0
  44. package/projects/angular-sdk/src/lib/components/audit-management/audit-management.component.ts +63 -0
  45. package/projects/angular-sdk/src/lib/components/descope/descope.component.spec.ts +142 -0
  46. package/projects/angular-sdk/src/lib/components/descope/descope.component.ts +171 -0
  47. package/projects/angular-sdk/src/lib/components/role-management/role-management.component.spec.ts +84 -0
  48. package/projects/angular-sdk/src/lib/components/role-management/role-management.component.ts +63 -0
  49. package/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.html +17 -0
  50. package/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.spec.ts +53 -0
  51. package/projects/angular-sdk/src/lib/components/sign-in-flow/sign-in-flow.component.ts +35 -0
  52. package/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.html +17 -0
  53. package/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.spec.ts +51 -0
  54. package/projects/angular-sdk/src/lib/components/sign-up-flow/sign-up-flow.component.ts +35 -0
  55. package/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.html +17 -0
  56. package/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.spec.ts +53 -0
  57. package/projects/angular-sdk/src/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.ts +35 -0
  58. package/projects/angular-sdk/src/lib/components/user-management/user-management.component.spec.ts +84 -0
  59. package/projects/angular-sdk/src/lib/components/user-management/user-management.component.ts +64 -0
  60. package/projects/angular-sdk/src/lib/components/user-profile/user-profile.component.spec.ts +93 -0
  61. package/projects/angular-sdk/src/lib/components/user-profile/user-profile.component.ts +77 -0
  62. package/projects/angular-sdk/src/lib/descope-auth.module.ts +61 -0
  63. package/projects/angular-sdk/src/lib/services/descope-auth.guard.spec.ts +76 -0
  64. package/projects/angular-sdk/src/lib/services/descope-auth.guard.ts +16 -0
  65. package/projects/angular-sdk/src/lib/services/descope-auth.service.spec.ts +309 -0
  66. package/projects/angular-sdk/src/lib/services/descope-auth.service.ts +200 -0
  67. package/projects/angular-sdk/src/lib/services/descope.interceptor.spec.ts +102 -0
  68. package/projects/angular-sdk/src/lib/services/descope.interceptor.ts +76 -0
  69. package/projects/angular-sdk/src/lib/types/types.ts +15 -0
  70. package/projects/angular-sdk/src/lib/utils/constants.ts +8 -0
  71. package/projects/angular-sdk/src/lib/utils/helpers.spec.ts +103 -0
  72. package/projects/angular-sdk/src/lib/utils/helpers.ts +36 -0
  73. package/projects/angular-sdk/src/public-api.ts +18 -0
  74. package/projects/angular-sdk/tsconfig.lib.json +12 -0
  75. package/projects/angular-sdk/tsconfig.lib.prod.json +10 -0
  76. package/projects/angular-sdk/tsconfig.spec.json +11 -0
  77. package/projects/demo-app/.eslintrc.json +31 -0
  78. package/projects/demo-app/src/app/app-routing.module.ts +33 -0
  79. package/projects/demo-app/src/app/app.component.html +3 -0
  80. package/projects/demo-app/src/app/app.component.scss +18 -0
  81. package/projects/demo-app/src/app/app.component.spec.ts +37 -0
  82. package/projects/demo-app/src/app/app.component.ts +8 -0
  83. package/projects/demo-app/src/app/app.module.ts +63 -0
  84. package/projects/demo-app/src/app/home/home.component.html +32 -0
  85. package/projects/demo-app/src/app/home/home.component.scss +15 -0
  86. package/projects/demo-app/src/app/home/home.component.spec.ts +44 -0
  87. package/projects/demo-app/src/app/home/home.component.ts +85 -0
  88. package/projects/demo-app/src/app/interceptor/auth.interceptor.ts +20 -0
  89. package/projects/demo-app/src/app/login/login.component.html +20 -0
  90. package/projects/demo-app/src/app/login/login.component.spec.ts +42 -0
  91. package/projects/demo-app/src/app/login/login.component.ts +41 -0
  92. package/projects/demo-app/src/app/manage-access-keys/manage-access-keys.component.html +12 -0
  93. package/projects/demo-app/src/app/manage-access-keys/manage-access-keys.component.spec.ts +36 -0
  94. package/projects/demo-app/src/app/manage-access-keys/manage-access-keys.component.ts +15 -0
  95. package/projects/demo-app/src/app/manage-audit/manage-audit.component.html +6 -0
  96. package/projects/demo-app/src/app/manage-audit/manage-audit.component.spec.ts +36 -0
  97. package/projects/demo-app/src/app/manage-audit/manage-audit.component.ts +15 -0
  98. package/projects/demo-app/src/app/manage-roles/manage-roles.component.html +6 -0
  99. package/projects/demo-app/src/app/manage-roles/manage-roles.component.spec.ts +36 -0
  100. package/projects/demo-app/src/app/manage-roles/manage-roles.component.ts +15 -0
  101. package/projects/demo-app/src/app/manage-users/manage-users.component.html +6 -0
  102. package/projects/demo-app/src/app/manage-users/manage-users.component.spec.ts +36 -0
  103. package/projects/demo-app/src/app/manage-users/manage-users.component.ts +15 -0
  104. package/projects/demo-app/src/app/my-user-profile/my-user-profile.component.html +6 -0
  105. package/projects/demo-app/src/app/my-user-profile/my-user-profile.component.spec.ts +36 -0
  106. package/projects/demo-app/src/app/my-user-profile/my-user-profile.component.ts +20 -0
  107. package/projects/demo-app/src/app/my-user-profile/my-user-profile.scss +18 -0
  108. package/projects/demo-app/src/app/protected/protected.component.html +18 -0
  109. package/projects/demo-app/src/app/protected/protected.component.scss +8 -0
  110. package/projects/demo-app/src/app/protected/protected.component.spec.ts +42 -0
  111. package/projects/demo-app/src/app/protected/protected.component.ts +41 -0
  112. package/projects/demo-app/src/assets/.gitkeep +0 -0
  113. package/projects/demo-app/src/environments/conifg.ts +14 -0
  114. package/projects/demo-app/src/environments/environment.ts +20 -0
  115. package/projects/demo-app/src/favicon.ico +0 -0
  116. package/projects/demo-app/src/index.html +17 -0
  117. package/projects/demo-app/src/main.ts +7 -0
  118. package/projects/demo-app/src/styles.scss +21 -0
  119. package/projects/demo-app/tsconfig.app.json +11 -0
  120. package/projects/demo-app/tsconfig.spec.json +10 -0
  121. package/scripts/setversion/setversion.js +20 -0
  122. package/setup-jest.ts +1 -0
  123. package/tsconfig.json +36 -0
  124. package/esm2022/lib/components/access-key-management/access-key-management.component.mjs +0 -57
  125. package/esm2022/lib/components/audit-management/audit-management.component.mjs +0 -57
  126. package/esm2022/lib/components/descope/descope.component.mjs +0 -152
  127. package/esm2022/lib/components/role-management/role-management.component.mjs +0 -57
  128. package/esm2022/lib/components/sign-in-flow/sign-in-flow.component.mjs +0 -44
  129. package/esm2022/lib/components/sign-up-flow/sign-up-flow.component.mjs +0 -44
  130. package/esm2022/lib/components/sign-up-or-in-flow/sign-up-or-in-flow.component.mjs +0 -44
  131. package/esm2022/lib/components/user-management/user-management.component.mjs +0 -57
  132. package/esm2022/lib/components/user-profile/user-profile.component.mjs +0 -62
  133. package/esm2022/lib/services/descope-auth.service.mjs +0 -154
  134. package/esm2022/lib/services/descope.interceptor.mjs +0 -51
  135. package/esm2022/lib/types/types.mjs +0 -6
  136. package/esm2022/lib/utils/helpers.mjs +0 -27
  137. package/fesm2022/descope-angular-sdk.mjs.map +0 -1
  138. /package/{environment.d.ts → dist/environment.d.ts} +0 -0
  139. /package/{esm2022 → dist/esm2022}/descope-angular-sdk.mjs +0 -0
  140. /package/{esm2022 → dist/esm2022}/public-api.mjs +0 -0
  141. /package/{index.d.ts → dist/index.d.ts} +0 -0
  142. /package/{lib → dist/lib}/components/sign-in-flow/sign-in-flow.component.d.ts +0 -0
  143. /package/{lib → dist/lib}/components/sign-up-flow/sign-up-flow.component.d.ts +0 -0
  144. /package/{lib → dist/lib}/components/sign-up-or-in-flow/sign-up-or-in-flow.component.d.ts +0 -0
  145. /package/{lib → dist/lib}/descope-auth.module.d.ts +0 -0
  146. /package/{lib → dist/lib}/services/descope-auth.guard.d.ts +0 -0
  147. /package/{lib → dist/lib}/services/descope.interceptor.d.ts +0 -0
  148. /package/{lib → dist/lib}/utils/constants.d.ts +0 -0
  149. /package/{lib → dist/lib}/utils/helpers.d.ts +0 -0
  150. /package/{public-api.d.ts → dist/public-api.d.ts} +0 -0
@@ -0,0 +1,309 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+
3
+ import { DescopeAuthService } from './descope-auth.service';
4
+ import createSdk from '@descope/web-js-sdk';
5
+ import mocked = jest.mocked;
6
+ import { DescopeAuthConfig } from '../types/types';
7
+ import { of, take, toArray } from 'rxjs';
8
+
9
+ jest.mock('@descope/web-js-sdk');
10
+
11
+ describe('DescopeAuthService', () => {
12
+ let service: DescopeAuthService;
13
+ let mockedCreateSdk: jest.Mock;
14
+ let windowSpy: jest.SpyInstance;
15
+ const onSessionTokenChangeSpy = jest.fn();
16
+ const onUserChangeSpy = jest.fn();
17
+ const getSessionTokenSpy = jest.fn();
18
+ const getRefreshTokenSpy = jest.fn();
19
+ const isJwtExpiredSpy = jest.fn();
20
+ const getJwtPermissionsSpy = jest.fn();
21
+ const getJwtRolesSpy = jest.fn();
22
+ const meSpy = jest.fn();
23
+ const refreshSpy = jest.fn();
24
+ const mockConfig: DescopeAuthConfig = {
25
+ projectId: 'someProject'
26
+ };
27
+
28
+ beforeEach(() => {
29
+ mockedCreateSdk = mocked(createSdk);
30
+ windowSpy = jest.spyOn(window, 'window', 'get');
31
+
32
+ mockedCreateSdk.mockReturnValue({
33
+ onSessionTokenChange: onSessionTokenChangeSpy,
34
+ onUserChange: onUserChangeSpy,
35
+ getSessionToken: getSessionTokenSpy,
36
+ getRefreshToken: getRefreshTokenSpy,
37
+ isJwtExpired: isJwtExpiredSpy,
38
+ getJwtPermissions: getJwtPermissionsSpy,
39
+ getJwtRoles: getJwtRolesSpy,
40
+ me: meSpy,
41
+ refresh: refreshSpy
42
+ });
43
+
44
+ onSessionTokenChangeSpy.mockImplementation((fn) => fn());
45
+ onUserChangeSpy.mockImplementation((fn) => fn());
46
+
47
+ TestBed.configureTestingModule({
48
+ providers: [
49
+ DescopeAuthConfig,
50
+ { provide: DescopeAuthConfig, useValue: mockConfig }
51
+ ]
52
+ });
53
+ service = TestBed.inject(DescopeAuthService);
54
+ });
55
+
56
+ afterEach(() => {
57
+ getSessionTokenSpy.mockReset();
58
+ getRefreshTokenSpy.mockReset();
59
+ isJwtExpiredSpy.mockReset();
60
+ getJwtPermissionsSpy.mockReset();
61
+ getJwtRolesSpy.mockReset();
62
+ });
63
+
64
+ it('should be created', () => {
65
+ expect(service).toBeTruthy();
66
+ expect(mockedCreateSdk).toHaveBeenCalledWith(
67
+ expect.objectContaining(mockConfig)
68
+ );
69
+ expect(onSessionTokenChangeSpy).toHaveBeenCalled();
70
+ expect(onUserChangeSpy).toHaveBeenCalled();
71
+ });
72
+
73
+ describe('getSessionToken', () => {
74
+ it('should call getSessionToken from sdk', () => {
75
+ const token = 'abcd';
76
+ getSessionTokenSpy.mockReturnValueOnce(token);
77
+ const result = service.getSessionToken();
78
+ expect(getSessionTokenSpy).toHaveBeenCalled();
79
+ expect(result).toStrictEqual(token);
80
+ });
81
+
82
+ it('should warn when using getSessionToken in non browser environment', () => {
83
+ const warnSpy = jest.spyOn(console, 'warn');
84
+ windowSpy.mockImplementationOnce(() => undefined);
85
+
86
+ service.getSessionToken();
87
+
88
+ expect(warnSpy).toHaveBeenCalledWith(
89
+ 'Get session token is not supported in SSR'
90
+ );
91
+ expect(getSessionTokenSpy).not.toHaveBeenCalled();
92
+ });
93
+ });
94
+
95
+ describe('getRefreshToken', () => {
96
+ it('should call getRefreshToken from sdk', () => {
97
+ const token = 'abcd';
98
+ getRefreshTokenSpy.mockReturnValueOnce(token);
99
+ const result = service.getRefreshToken();
100
+ expect(getRefreshTokenSpy).toHaveBeenCalled();
101
+ expect(result).toStrictEqual(token);
102
+ });
103
+
104
+ it('should warn when using getRefreshToken in non browser environment', () => {
105
+ const warnSpy = jest.spyOn(console, 'warn');
106
+ windowSpy.mockImplementationOnce(() => undefined);
107
+
108
+ service.getRefreshToken();
109
+
110
+ expect(warnSpy).toHaveBeenCalledWith(
111
+ 'Get refresh token is not supported in SSR'
112
+ );
113
+ expect(getRefreshTokenSpy).not.toHaveBeenCalled();
114
+ });
115
+ });
116
+
117
+ describe('isSessionTokenExpired', () => {
118
+ it('should call isSessionTokenExpired from sdk', () => {
119
+ const token = 'abcd';
120
+ getSessionTokenSpy.mockReturnValueOnce(token);
121
+ service.isSessionTokenExpired();
122
+ expect(getSessionTokenSpy).toHaveBeenCalled();
123
+ expect(isJwtExpiredSpy).toHaveBeenCalledWith(token);
124
+ });
125
+
126
+ it('should warn when using isSessionTokenExpired in non browser environment', () => {
127
+ const warnSpy = jest.spyOn(console, 'warn');
128
+ windowSpy.mockImplementationOnce(() => undefined);
129
+
130
+ service.isSessionTokenExpired('some token');
131
+ expect(warnSpy).toHaveBeenCalledWith(
132
+ 'isSessionTokenExpired is not supported in SSR'
133
+ );
134
+ expect(isJwtExpiredSpy).not.toHaveBeenCalled();
135
+ });
136
+ });
137
+
138
+ describe('isRefreshTokenExpired', () => {
139
+ it('should call isRefreshTokenExpired from sdk', () => {
140
+ const token = 'abcd';
141
+ getRefreshTokenSpy.mockReturnValueOnce(token);
142
+ service.isRefreshTokenExpired();
143
+ expect(getRefreshTokenSpy).toHaveBeenCalled();
144
+ expect(isJwtExpiredSpy).toHaveBeenCalledWith(token);
145
+ });
146
+
147
+ it('should warn when using isRefreshTokenExpired in non browser environment', () => {
148
+ const warnSpy = jest.spyOn(console, 'warn');
149
+ windowSpy.mockImplementationOnce(() => undefined);
150
+
151
+ service.isRefreshTokenExpired('some token');
152
+ expect(warnSpy).toHaveBeenCalledWith(
153
+ 'isRefreshTokenExpired is not supported in SSR'
154
+ );
155
+ expect(isJwtExpiredSpy).not.toHaveBeenCalled();
156
+ });
157
+ });
158
+
159
+ describe('getJwtPermissions', () => {
160
+ it('should return permissions for token from sdk', () => {
161
+ const permissions = ['edit'];
162
+ getJwtPermissionsSpy.mockReturnValueOnce(permissions);
163
+ const result = service.getJwtPermissions('token');
164
+ expect(getJwtPermissionsSpy).toHaveBeenCalledWith('token', undefined);
165
+ expect(result).toStrictEqual(permissions);
166
+ });
167
+
168
+ it('should return empty array and log error when there is no token', () => {
169
+ const errorSpy = jest.spyOn(console, 'error');
170
+ getSessionTokenSpy.mockReturnValueOnce(null);
171
+ const result = service.getJwtPermissions();
172
+ expect(errorSpy).toHaveBeenCalledWith(
173
+ 'Could not get JWT Permissions - not authenticated'
174
+ );
175
+ expect(getJwtPermissionsSpy).not.toHaveBeenCalled();
176
+ expect(result).toStrictEqual([]);
177
+ });
178
+ });
179
+
180
+ describe('getJwtRoles', () => {
181
+ it('should return roles for token from sdk', () => {
182
+ const roles = ['admin'];
183
+ getJwtRolesSpy.mockReturnValueOnce(roles);
184
+ const result = service.getJwtRoles('token');
185
+ expect(getJwtRolesSpy).toHaveBeenCalledWith('token', undefined);
186
+ expect(result).toStrictEqual(roles);
187
+ });
188
+
189
+ it('should return empty array and log error when there is no token', () => {
190
+ const errorSpy = jest.spyOn(console, 'error');
191
+ getSessionTokenSpy.mockReturnValueOnce(null);
192
+ const result = service.getJwtRoles();
193
+ expect(errorSpy).toHaveBeenCalledWith(
194
+ 'Could not get JWT Roles - not authenticated'
195
+ );
196
+ expect(getJwtRolesSpy).not.toHaveBeenCalled();
197
+ expect(result).toStrictEqual([]);
198
+ });
199
+ });
200
+
201
+ describe('refreshSession', () => {
202
+ it('correctly handle descopeSession stream when session is successfully refreshed', (done: jest.DoneCallback) => {
203
+ refreshSpy.mockReturnValueOnce(
204
+ of({ ok: true, data: { sessionJwt: 'newToken' } })
205
+ );
206
+ // Taking 4 values from stream: first is initial value, next 3 are the result of refreshSession
207
+ service.session$.pipe(take(4), toArray()).subscribe({
208
+ next: (result) => {
209
+ expect(result.slice(1)).toStrictEqual([
210
+ {
211
+ isAuthenticated: false,
212
+ isSessionLoading: true,
213
+ sessionToken: undefined
214
+ },
215
+ {
216
+ isAuthenticated: true,
217
+ isSessionLoading: true,
218
+ sessionToken: 'newToken'
219
+ },
220
+ {
221
+ isAuthenticated: true,
222
+ isSessionLoading: false,
223
+ sessionToken: 'newToken'
224
+ }
225
+ ]);
226
+ expect(service.isAuthenticated()).toBeTruthy();
227
+ done();
228
+ },
229
+ error: (err) => {
230
+ done.fail(err);
231
+ }
232
+ });
233
+ service.refreshSession().subscribe();
234
+ });
235
+
236
+ it('correctly handle descopeSession stream when refresh session failed', (done: jest.DoneCallback) => {
237
+ refreshSpy.mockReturnValueOnce(
238
+ of({ ok: false, data: { sessionJwt: 'newToken' } })
239
+ );
240
+ // Taking 4 values from stream: first is initial value, next 3 are the result of refreshSession
241
+ service.session$.pipe(take(4), toArray()).subscribe({
242
+ next: (result) => {
243
+ expect(result.slice(1)).toStrictEqual([
244
+ {
245
+ isAuthenticated: false,
246
+ isSessionLoading: true,
247
+ sessionToken: undefined
248
+ },
249
+ {
250
+ isAuthenticated: false,
251
+ isSessionLoading: true,
252
+ sessionToken: ''
253
+ },
254
+ {
255
+ isAuthenticated: false,
256
+ isSessionLoading: false,
257
+ sessionToken: ''
258
+ }
259
+ ]);
260
+ expect(service.isAuthenticated()).toBeFalsy();
261
+ done();
262
+ },
263
+ error: (err) => {
264
+ done.fail(err);
265
+ }
266
+ });
267
+ service.refreshSession().subscribe();
268
+ });
269
+ });
270
+
271
+ describe('refreshUser', () => {
272
+ it('correctly handle descopeUser stream when user is successfully refreshed', (done: jest.DoneCallback) => {
273
+ meSpy.mockReturnValueOnce(of({ ok: true, data: { name: 'test' } }));
274
+ // Taking 4 values from stream: first is initial value, next 3 are the result of refreshUser
275
+ service.user$.pipe(take(4), toArray()).subscribe({
276
+ next: (result) => {
277
+ expect(result.slice(1)).toStrictEqual([
278
+ { isUserLoading: true, user: undefined },
279
+ { isUserLoading: true, user: { name: 'test' } },
280
+ { isUserLoading: false, user: { name: 'test' } }
281
+ ]);
282
+ done();
283
+ },
284
+ error: (err) => {
285
+ done.fail(err);
286
+ }
287
+ });
288
+ service.refreshUser().subscribe();
289
+ });
290
+
291
+ it('correctly handle descopeUser stream when refresh session failed', (done: jest.DoneCallback) => {
292
+ meSpy.mockReturnValueOnce(of({ ok: false }));
293
+ // Taking 3 values from stream: first is initial value, next 2 are the result of refreshUser
294
+ service.user$.pipe(take(3), toArray()).subscribe({
295
+ next: (result) => {
296
+ expect(result.slice(1)).toStrictEqual([
297
+ { isUserLoading: true, user: undefined },
298
+ { isUserLoading: false, user: undefined }
299
+ ]);
300
+ done();
301
+ },
302
+ error: (err) => {
303
+ done.fail(err);
304
+ }
305
+ });
306
+ service.refreshUser().subscribe();
307
+ });
308
+ });
309
+ });
@@ -0,0 +1,200 @@
1
+ // workaround for TS issue https://github.com/microsoft/TypeScript/issues/42873
2
+ // eslint-disable-next-line
3
+ import type * as _1 from '@descope/core-js-sdk';
4
+ import { Injectable } from '@angular/core';
5
+ import type { UserResponse } from '@descope/web-js-sdk';
6
+ import createSdk from '@descope/web-js-sdk';
7
+ import { BehaviorSubject, finalize, Observable, tap } from 'rxjs';
8
+ import { observabilify, Observablefied } from '../utils/helpers';
9
+ import { baseHeaders, isBrowser } from '../utils/constants';
10
+ import { DescopeAuthConfig } from '../types/types';
11
+
12
+ type DescopeSDK = ReturnType<typeof createSdk>;
13
+ type AngularDescopeSDK = Observablefied<DescopeSDK>;
14
+
15
+ export interface DescopeSession {
16
+ isAuthenticated: boolean;
17
+ isSessionLoading: boolean;
18
+ sessionToken: string | null;
19
+ }
20
+
21
+ export type DescopeUser = { user?: UserResponse; isUserLoading: boolean };
22
+
23
+ @Injectable({
24
+ providedIn: 'root'
25
+ })
26
+ export class DescopeAuthService {
27
+ public descopeSdk: AngularDescopeSDK;
28
+ private readonly sessionSubject: BehaviorSubject<DescopeSession>;
29
+ private readonly userSubject: BehaviorSubject<DescopeUser>;
30
+ readonly session$: Observable<DescopeSession>;
31
+ readonly user$: Observable<DescopeUser>;
32
+
33
+ constructor(config: DescopeAuthConfig) {
34
+ this.descopeSdk = observabilify<DescopeSDK>(
35
+ createSdk({
36
+ persistTokens: isBrowser() as true,
37
+ ...config,
38
+ storeLastAuthenticatedUser: isBrowser() as true,
39
+ autoRefresh: isBrowser() as true,
40
+ baseHeaders
41
+ })
42
+ );
43
+
44
+ this.sessionSubject = new BehaviorSubject<DescopeSession>({
45
+ isAuthenticated: false,
46
+ isSessionLoading: false,
47
+ sessionToken: ''
48
+ });
49
+ this.session$ = this.sessionSubject.asObservable();
50
+ this.userSubject = new BehaviorSubject<DescopeUser>({
51
+ isUserLoading: false
52
+ });
53
+ this.user$ = this.userSubject.asObservable();
54
+ this.descopeSdk.onSessionTokenChange(this.setSession.bind(this));
55
+ this.descopeSdk.onUserChange(this.setUser.bind(this));
56
+ }
57
+
58
+ refreshSession() {
59
+ const beforeRefreshSession = this.sessionSubject.value;
60
+ this.sessionSubject.next({
61
+ ...beforeRefreshSession,
62
+ isSessionLoading: true
63
+ });
64
+ return this.descopeSdk.refresh().pipe(
65
+ tap((data) => {
66
+ const afterRequestSession = this.sessionSubject.value;
67
+ if (data.ok && data.data) {
68
+ this.sessionSubject.next({
69
+ ...afterRequestSession,
70
+ sessionToken: data.data.sessionJwt,
71
+ isAuthenticated: !!data.data.sessionJwt
72
+ });
73
+ } else {
74
+ this.sessionSubject.next({
75
+ ...afterRequestSession,
76
+ sessionToken: '',
77
+ isAuthenticated: false
78
+ });
79
+ }
80
+ }),
81
+ finalize(() => {
82
+ const afterRefreshSession = this.sessionSubject.value;
83
+ this.sessionSubject.next({
84
+ ...afterRefreshSession,
85
+ isSessionLoading: false
86
+ });
87
+ })
88
+ );
89
+ }
90
+
91
+ refreshUser() {
92
+ const beforeRefreshUser = this.userSubject.value;
93
+ this.userSubject.next({
94
+ ...beforeRefreshUser,
95
+ isUserLoading: true
96
+ });
97
+ return this.descopeSdk.me().pipe(
98
+ tap((data) => {
99
+ const afterRequestUser = this.userSubject.value;
100
+ if (data.data) {
101
+ this.userSubject.next({
102
+ ...afterRequestUser,
103
+ user: {
104
+ ...data.data
105
+ }
106
+ });
107
+ }
108
+ }),
109
+ finalize(() => {
110
+ const afterRefreshUser = this.userSubject.value;
111
+ this.userSubject.next({
112
+ ...afterRefreshUser,
113
+ isUserLoading: false
114
+ });
115
+ })
116
+ );
117
+ }
118
+
119
+ getSessionToken() {
120
+ if (isBrowser()) {
121
+ return (
122
+ this.descopeSdk as AngularDescopeSDK & {
123
+ getSessionToken: () => string | null;
124
+ }
125
+ ).getSessionToken();
126
+ }
127
+ console.warn('Get session token is not supported in SSR');
128
+ return '';
129
+ }
130
+
131
+ getRefreshToken() {
132
+ if (isBrowser()) {
133
+ return (
134
+ this.descopeSdk as AngularDescopeSDK & {
135
+ getRefreshToken: () => string | null;
136
+ }
137
+ ).getRefreshToken();
138
+ }
139
+ this.descopeSdk.getJwtPermissions;
140
+ console.warn('Get refresh token is not supported in SSR');
141
+ return '';
142
+ }
143
+
144
+ isSessionTokenExpired(token = this.getSessionToken()) {
145
+ if (isBrowser()) {
146
+ return this.descopeSdk.isJwtExpired(token ?? '');
147
+ }
148
+ console.warn('isSessionTokenExpired is not supported in SSR');
149
+ return true;
150
+ }
151
+
152
+ isRefreshTokenExpired(token = this.getRefreshToken()) {
153
+ if (isBrowser()) {
154
+ return (
155
+ this.descopeSdk as AngularDescopeSDK & {
156
+ isJwtExpired: (token: string) => boolean | null;
157
+ }
158
+ ).isJwtExpired(token ?? '');
159
+ }
160
+ console.warn('isRefreshTokenExpired is not supported in SSR');
161
+ return true;
162
+ }
163
+
164
+ getJwtPermissions(token = this.getSessionToken(), tenant?: string) {
165
+ if (token === null) {
166
+ console.error('Could not get JWT Permissions - not authenticated');
167
+ return [];
168
+ }
169
+ return this.descopeSdk.getJwtPermissions(token, tenant);
170
+ }
171
+
172
+ getJwtRoles(token = this.getSessionToken(), tenant?: string) {
173
+ if (token === null) {
174
+ console.error('Could not get JWT Roles - not authenticated');
175
+ return [];
176
+ }
177
+ return this.descopeSdk.getJwtRoles(token, tenant);
178
+ }
179
+
180
+ isAuthenticated() {
181
+ return this.sessionSubject.value.isAuthenticated;
182
+ }
183
+
184
+ private setSession(sessionToken: string | null) {
185
+ const currentSession = this.sessionSubject.value;
186
+ this.sessionSubject.next({
187
+ sessionToken,
188
+ isAuthenticated: !!sessionToken,
189
+ isSessionLoading: currentSession.isSessionLoading
190
+ });
191
+ }
192
+
193
+ private setUser(user: UserResponse) {
194
+ const currentUser = this.userSubject.value;
195
+ this.userSubject.next({
196
+ isUserLoading: currentUser.isUserLoading,
197
+ user
198
+ });
199
+ }
200
+ }
@@ -0,0 +1,102 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import {
3
+ HttpTestingController,
4
+ provideHttpClientTesting
5
+ } from '@angular/common/http/testing';
6
+ import {
7
+ HttpClient,
8
+ provideHttpClient,
9
+ withInterceptors
10
+ } from '@angular/common/http';
11
+ import { of } from 'rxjs';
12
+ import { DescopeAuthService } from './descope-auth.service';
13
+ import { DescopeAuthConfig } from '../types/types';
14
+ import createSdk from '@descope/web-js-sdk';
15
+ import { descopeInterceptor } from './descope.interceptor';
16
+ import mocked = jest.mocked;
17
+
18
+ jest.mock('@descope/web-js-sdk');
19
+
20
+ describe('DescopeInterceptor', () => {
21
+ let authService: DescopeAuthService;
22
+ let httpTestingController: HttpTestingController;
23
+ let httpClient: HttpClient;
24
+ let mockedCreateSdk: jest.Mock;
25
+
26
+ beforeEach(() => {
27
+ mockedCreateSdk = mocked(createSdk);
28
+ mockedCreateSdk.mockReturnValue({
29
+ onSessionTokenChange: jest.fn(),
30
+ onUserChange: jest.fn()
31
+ });
32
+
33
+ TestBed.configureTestingModule({
34
+ providers: [
35
+ DescopeAuthService,
36
+ {
37
+ provide: DescopeAuthConfig,
38
+ useValue: { pathsToIntercept: ['/api'], projectId: 'test' }
39
+ },
40
+ provideHttpClient(withInterceptors([descopeInterceptor])),
41
+ provideHttpClientTesting()
42
+ ]
43
+ });
44
+
45
+ authService = TestBed.inject(DescopeAuthService);
46
+ httpTestingController = TestBed.inject(HttpTestingController);
47
+ httpClient = TestBed.inject(HttpClient);
48
+ });
49
+
50
+ afterEach(() => {
51
+ httpTestingController.verify();
52
+ });
53
+
54
+ it('should intercept requests for specified paths', () => {
55
+ jest.spyOn(authService, 'getSessionToken').mockReturnValue('fakeToken');
56
+
57
+ httpClient.get('/api/data').subscribe();
58
+ httpClient.get('/other').subscribe();
59
+
60
+ const req1 = httpTestingController.expectOne('/api/data');
61
+ const req2 = httpTestingController.expectOne('/other');
62
+
63
+ expect(req1.request.headers.get('Authorization')).toEqual(
64
+ 'Bearer fakeToken'
65
+ );
66
+ expect(req2.request.headers.get('Authorization')).toEqual(null);
67
+ req1.flush({});
68
+ req2.flush({});
69
+ });
70
+
71
+ it('should refresh token and retry request on 401 or 403 error', () => {
72
+ jest.spyOn(authService, 'getSessionToken').mockReturnValue(null);
73
+ const refreshSessionSpy = jest
74
+ .spyOn(authService, 'refreshSession')
75
+ .mockReturnValue(of({ ok: true, data: { sessionJwt: 'newToken' } }));
76
+
77
+ httpClient.get('/api/data').subscribe();
78
+
79
+ const req = httpTestingController.expectOne('/api/data');
80
+
81
+ expect(req.request.headers.get('Authorization')).toEqual('Bearer newToken');
82
+ expect(refreshSessionSpy).toHaveBeenCalled();
83
+ req.flush({}, { status: 401, statusText: 'Not authorized' });
84
+ });
85
+
86
+ it('should throw an error if refreshing the session fails', () => {
87
+ jest.spyOn(authService, 'getSessionToken').mockReturnValue(null);
88
+ jest
89
+ .spyOn(authService, 'refreshSession')
90
+ .mockReturnValue(of({ ok: false, data: undefined }));
91
+
92
+ httpClient.get('/api/data').subscribe({
93
+ next: () => {},
94
+ error: (error) => {
95
+ expect(error.message).toEqual('Could not refresh session!');
96
+ },
97
+ complete: () => {}
98
+ });
99
+
100
+ httpTestingController.expectNone('/api/data');
101
+ });
102
+ });
@@ -0,0 +1,76 @@
1
+ import { inject } from '@angular/core';
2
+ import {
3
+ HttpErrorResponse,
4
+ HttpHandlerFn,
5
+ HttpInterceptorFn,
6
+ HttpRequest
7
+ } from '@angular/common/http';
8
+ import { throwError } from 'rxjs';
9
+ import { catchError, switchMap } from 'rxjs/operators';
10
+ import { DescopeAuthService } from './descope-auth.service';
11
+ import { DescopeAuthConfig } from '../types/types';
12
+
13
+ export const descopeInterceptor: HttpInterceptorFn = (request, next) => {
14
+ const config = inject(DescopeAuthConfig);
15
+ const authService = inject(DescopeAuthService);
16
+
17
+ function refreshAndRetry(
18
+ request: HttpRequest<unknown>,
19
+ next: HttpHandlerFn,
20
+ error?: HttpErrorResponse
21
+ ) {
22
+ return authService.refreshSession().pipe(
23
+ switchMap((refreshed) => {
24
+ if (refreshed.ok && refreshed.data) {
25
+ const requestWithRefreshedToken = addTokenToRequest(
26
+ request,
27
+ refreshed.data?.sessionJwt
28
+ );
29
+ return next(requestWithRefreshedToken);
30
+ } else {
31
+ return throwError(
32
+ () => error ?? new Error('Could not refresh session!')
33
+ );
34
+ }
35
+ })
36
+ );
37
+ }
38
+
39
+ function shouldIntercept(request: HttpRequest<unknown>): boolean {
40
+ return (
41
+ (config.pathsToIntercept?.length === 0 ||
42
+ config.pathsToIntercept?.some((path) => request.url.includes(path))) ??
43
+ true
44
+ );
45
+ }
46
+
47
+ function addTokenToRequest(
48
+ request: HttpRequest<unknown>,
49
+ token: string
50
+ ): HttpRequest<unknown> {
51
+ return request.clone({
52
+ setHeaders: {
53
+ Authorization: `Bearer ${token}`
54
+ }
55
+ });
56
+ }
57
+
58
+ if (shouldIntercept(request)) {
59
+ const token = authService.getSessionToken();
60
+ if (!token) {
61
+ return refreshAndRetry(request, next);
62
+ }
63
+ const requestWithToken = addTokenToRequest(request, token);
64
+ return next(requestWithToken).pipe(
65
+ catchError((error: HttpErrorResponse) => {
66
+ if (error.status === 401 || error.status === 403) {
67
+ return refreshAndRetry(request, next, error);
68
+ } else {
69
+ return throwError(() => error);
70
+ }
71
+ })
72
+ );
73
+ } else {
74
+ return next(request);
75
+ }
76
+ };
@@ -0,0 +1,15 @@
1
+ import { ILogger } from '@descope/web-component';
2
+
3
+ export class DescopeAuthConfig {
4
+ projectId = '';
5
+ baseUrl?: string;
6
+ baseStaticUrl?: string;
7
+ // If true, tokens will be stored on local storage
8
+ persistTokens?: boolean;
9
+ sessionTokenViaCookie?: boolean;
10
+ // If true, last authenticated user will be stored on local storage and can accessed with getUser function
11
+ storeLastAuthenticatedUser?: boolean;
12
+ pathsToIntercept?: string[];
13
+ }
14
+
15
+ export type { ILogger };