zavadil-ts-common 1.1.47 → 1.1.49

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.
@@ -18,7 +18,7 @@ export class RestClient {
18
18
  /**
19
19
  * Override this to customize http headers.
20
20
  */
21
- getHeaders(): Promise<RestClientHeaders> {
21
+ getHeaders(url: string): Promise<RestClientHeaders> {
22
22
  return Promise.resolve(
23
23
  {
24
24
  'Content-Type': 'application/json'
@@ -46,8 +46,8 @@ export class RestClient {
46
46
  return url;
47
47
  }
48
48
 
49
- getRequestOptions(method: string = 'GET', data: object | null = null): Promise<RequestInit> {
50
- return this.getHeaders()
49
+ getRequestOptions(url: string, method: string = 'GET', data: object | null = null): Promise<RequestInit> {
50
+ return this.getHeaders(url)
51
51
  .then(
52
52
  (headers) => {
53
53
  return {
@@ -116,31 +116,31 @@ export class RestClient {
116
116
  if (params) {
117
117
  url = `${url}${this.paramsToQueryString(params)}`;
118
118
  }
119
- return this.getRequestOptions().then(o => this.processRequestJson(url, o));
119
+ return this.getRequestOptions(url).then(o => this.processRequestJson(url, o));
120
120
  }
121
121
 
122
122
  postJson(url: string, data: object | null = null): Promise<any> {
123
- return this.getRequestOptions('POST', data).then(o => this.processRequestJson(url, o));
123
+ return this.getRequestOptions(url, 'POST', data).then(o => this.processRequestJson(url, o));
124
124
  }
125
125
 
126
126
  putJson(url: string, data: object | null = null): Promise<any> {
127
- return this.getRequestOptions('PUT', data).then(o => this.processRequestJson(url, o));
127
+ return this.getRequestOptions(url, 'PUT', data).then(o => this.processRequestJson(url, o));
128
128
  }
129
129
 
130
130
  get(url: string): Promise<Response> {
131
- return this.getRequestOptions().then(o => this.processRequest(url, o));
131
+ return this.getRequestOptions(url).then(o => this.processRequest(url, o));
132
132
  }
133
133
 
134
134
  del(url: string): Promise<Response> {
135
- return this.getRequestOptions('DELETE').then(o => this.processRequest(url, o));
135
+ return this.getRequestOptions(url, 'DELETE').then(o => this.processRequest(url, o));
136
136
  }
137
137
 
138
138
  post(url: string, data: object | null = null): Promise<Response> {
139
- return this.getRequestOptions('POST', data).then(o => this.processRequest(url, o));
139
+ return this.getRequestOptions(url, 'POST', data).then(o => this.processRequest(url, o));
140
140
  }
141
141
 
142
142
  put(url: string, data: object | null = null): Promise<Response> {
143
- return this.getRequestOptions('PUT', data).then(o => this.processRequest(url, o));
143
+ return this.getRequestOptions(url, 'PUT', data).then(o => this.processRequest(url, o));
144
144
  }
145
145
 
146
146
  }
@@ -1,4 +1,4 @@
1
- import {RestClient} from "../client/RestClient";
1
+ import {RestClient} from "../client";
2
2
  import {StringUtil} from "../util";
3
3
 
4
4
  export type TokenRequestPayloadBase = {
@@ -7,6 +7,7 @@ export type TokenRequestPayloadBase = {
7
7
 
8
8
  export type RequestAccessTokenPayload = TokenRequestPayloadBase & {
9
9
  idToken: string;
10
+ privilege: string;
10
11
  }
11
12
 
12
13
  export type RequestIdTokenFromPrevTokenPayload = {
@@ -29,7 +30,6 @@ export type IdTokenPayload = TokenResponsePayloadBase & {
29
30
 
30
31
  export type AccessTokenPayload = TokenResponsePayloadBase & {
31
32
  accessToken: string;
32
- refreshToken: string;
33
33
  }
34
34
 
35
35
  export type JwKeyPayload = {
@@ -45,7 +45,7 @@ export type JwksPayload = {
45
45
 
46
46
  /**
47
47
  * This implements rest client for OAuth server - https://github.com/lotcz/oauth-server
48
- * Provide basic url, /api/oauth prefix will be added automatically
48
+ * Provide basic url, /api/oauth path prefix will be added automatically
49
49
  */
50
50
  export class OAuthRestClient extends RestClient {
51
51
 
@@ -57,6 +57,10 @@ export class OAuthRestClient extends RestClient {
57
57
  return this.getJson('jwks.json');
58
58
  }
59
59
 
60
+ verifyIdToken(idToken: string): Promise<IdTokenPayload> {
61
+ return this.getJson(`id-tokens/verify/${idToken}`);
62
+ }
63
+
60
64
  requestIdTokenFromLogin(request: RequestIdTokenFromLoginPayload): Promise<IdTokenPayload> {
61
65
  return this.postJson('id-tokens/from-login', request);
62
66
  }
@@ -1,21 +1,29 @@
1
- import {AccessTokenPayload, IdTokenPayload, OAuthRestClient, RequestIdTokenFromLoginPayload} from "./OAuthRestClient";
1
+ import {AccessTokenPayload, IdTokenPayload, OAuthRestClient} from "./OAuthRestClient";
2
+ import {EventManager} from "../component";
2
3
 
3
4
  /**
4
5
  * Manages refresh of id and access tokens.
5
6
  */
6
7
  export class OAuthTokenManager {
7
8
 
9
+ private eventManager: EventManager = new EventManager();
10
+
8
11
  oAuthServer: OAuthRestClient;
9
12
 
10
13
  audience: string;
11
14
 
12
15
  idToken?: IdTokenPayload;
13
16
 
14
- accessToken?: AccessTokenPayload;
17
+ accessTokens: Map<string, AccessTokenPayload>;
15
18
 
16
19
  constructor(oAuthServerBaseUrl: string, targetAudience: string) {
17
20
  this.audience = targetAudience;
18
21
  this.oAuthServer = new OAuthRestClient(oAuthServerBaseUrl);
22
+ this.accessTokens = new Map<string, AccessTokenPayload>();
23
+ }
24
+
25
+ addIdTokenChangedHandler(handler: (t: IdTokenPayload) => any) {
26
+ this.eventManager.addEventListener('id-token-changed', handler);
19
27
  }
20
28
 
21
29
  isTokenExpired(expires?: Date | null): boolean {
@@ -44,13 +52,13 @@ export class OAuthTokenManager {
44
52
  return accessToken !== undefined && !this.isTokenExpired(accessToken.expires);
45
53
  }
46
54
 
47
- hasValidAccessToken(): boolean {
48
- return this.isValidAccessToken(this.accessToken);
55
+ hasValidAccessToken(privilege: string): boolean {
56
+ return this.isValidAccessToken(this.accessTokens.get(privilege));
49
57
  }
50
58
 
51
59
  reset() {
52
- this.idToken = undefined;
53
- this.accessToken = undefined;
60
+ this.setIdToken(undefined);
61
+ this.accessTokens.clear();
54
62
  }
55
63
 
56
64
  getIdToken(): Promise<string> {
@@ -62,10 +70,7 @@ export class OAuthTokenManager {
62
70
  .refreshIdToken({idToken: this.idToken.idToken})
63
71
  .then(
64
72
  (t) => {
65
- if (!this.isValidIdToken(t)) {
66
- return Promise.reject("Received ID token is not valid!");
67
- }
68
- this.idToken = t;
73
+ this.setIdToken(t);
69
74
  return t.idToken;
70
75
  }
71
76
  )
@@ -73,11 +78,18 @@ export class OAuthTokenManager {
73
78
  return Promise.resolve(this.idToken.idToken);
74
79
  }
75
80
 
76
- setIdToken(token: IdTokenPayload) {
81
+ setIdToken(token?: IdTokenPayload) {
77
82
  if (!this.isValidIdToken(token)) {
78
83
  throw new Error("Received ID token is not valid!");
79
84
  }
80
85
  this.idToken = token;
86
+ this.eventManager.triggerEvent('id-token-changed', token);
87
+ }
88
+
89
+ verifyIdToken(token: string): Promise<boolean> {
90
+ return this.oAuthServer.verifyIdToken(token)
91
+ .then((t) => this.setIdToken(t))
92
+ .then(() => true)
81
93
  }
82
94
 
83
95
  login(login: string, password: string): Promise<boolean> {
@@ -90,23 +102,27 @@ export class OAuthTokenManager {
90
102
  })
91
103
  }
92
104
 
93
- getAccessToken(): Promise<string> {
94
- if (this.hasValidAccessToken()) {
95
- return Promise.resolve(String(this.accessToken?.accessToken));
96
- } else {
97
- return this.getIdToken()
98
- .then(
99
- (idToken: string) => this.oAuthServer
100
- .requestAccessToken({idToken: idToken, targetAudience: this.audience})
101
- .then((act: AccessTokenPayload) => {
102
- if (!this.isValidAccessToken(act)) {
103
- return Promise.reject("Received access token is not valid!");
104
- }
105
- this.accessToken = act;
106
- return act.accessToken;
107
- })
108
- );
109
- }
105
+ private getAccessTokenInternal(privilege: string): Promise<AccessTokenPayload> {
106
+ return this.getIdToken()
107
+ .then(
108
+ (idToken: string) => this.oAuthServer
109
+ .requestAccessToken({idToken: idToken, targetAudience: this.audience, privilege: privilege})
110
+ .then((act: AccessTokenPayload) => {
111
+ if (!this.isValidAccessToken(act)) {
112
+ return Promise.reject("Received access token is not valid!");
113
+ }
114
+ this.accessTokens.set(privilege, act);
115
+ return act;
116
+ })
117
+ );
118
+ }
119
+
120
+ getAccessToken(privilege: string): Promise<string> {
121
+ const existing = this.accessTokens.get(privilege);
122
+ if (existing === undefined || !this.isValidAccessToken(existing)) return this.getAccessTokenInternal(privilege).then((t) => t.accessToken);
123
+ // preload access token if it is going to expire soon
124
+ if (this.isTokenReadyForRefresh(existing.issuedAt, existing.expires)) this.getAccessTokenInternal(privilege);
125
+ return Promise.resolve(existing.accessToken);
110
126
  }
111
127
 
112
128
  }
@@ -1,6 +1,7 @@
1
1
  import {OAuthTokenManager} from "./OAuthTokenManager";
2
- import { RestClient, RestClientHeaders } from "../client";
2
+ import {RestClient, RestClientHeaders} from "../client";
3
3
  import {IdTokenPayload} from "./OAuthRestClient";
4
+ import {LazyAsync} from "../cache";
4
5
 
5
6
  export type ServerOAuthInfoPayload = {
6
7
  debugMode?: boolean;
@@ -11,35 +12,87 @@ export type ServerOAuthInfoPayload = {
11
12
 
12
13
  export class RestClientWithOAuth extends RestClient {
13
14
 
14
- private tokenManager?: OAuthTokenManager;
15
+ private insecureClient: RestClient;
15
16
 
16
- private serverInfo?: ServerOAuthInfoPayload;
17
+ private tokenManager: LazyAsync<OAuthTokenManager>;
17
18
 
18
- private insecureClient: RestClient;
19
+ private serverInfo: LazyAsync<ServerOAuthInfoPayload>;
19
20
 
20
- constructor(url: string) {
21
+ private defaultPrivilege: string;
22
+
23
+ constructor(url: string, defaultPrivilege: string = '*') {
21
24
  super(url);
25
+ this.defaultPrivilege = defaultPrivilege;
26
+
22
27
  // rest client without OAuth headers
23
28
  this.insecureClient = new RestClient(url);
24
- }
25
29
 
26
- getServerInfo(): Promise<ServerOAuthInfoPayload> {
27
- if (this.serverInfo !== undefined) {
28
- return Promise.resolve(this.serverInfo);
30
+ this.serverInfo = new LazyAsync<ServerOAuthInfoPayload>(() => this.getServerInfoInternal());
31
+ this.tokenManager = new LazyAsync<OAuthTokenManager>(() => this.getTokenManagerInternal());
32
+
33
+ // initialize
34
+ const urlToken = this.getIdTokenFromUrl();
35
+ if (urlToken !== null) {
36
+ this.setIdTokenRaw(urlToken);
37
+ } else {
38
+ const storageToken = this.getIdTokenFromLocalStorage();
39
+ if (storageToken) this.setIdToken(storageToken);
29
40
  }
30
- return this.insecureClient.getJson('status/info')
31
- .then((si: ServerOAuthInfoPayload) => {
32
- this.serverInfo = si;
33
- return this.serverInfo;
34
- });
35
41
  }
36
42
 
37
- getTokenManager(): Promise<OAuthTokenManager> {
38
- if (this.tokenManager !== undefined) {
39
- return Promise.resolve(this.tokenManager);
43
+ /**
44
+ * Override this if a different privilege is needed for different endpoints
45
+ * @param url
46
+ */
47
+ getPrivilege(url: string): string {
48
+ return this.defaultPrivilege;
49
+ }
50
+
51
+ getIdTokenFromUrl(): string | null {
52
+ const up = new URLSearchParams(document.location.search);
53
+ return up.get('token');
54
+ }
55
+
56
+ getIdTokenFromLocalStorage(): IdTokenPayload | null {
57
+ const raw = localStorage.getItem('id-token');
58
+ if (!raw) return null;
59
+ return JSON.parse(raw);
60
+ }
61
+
62
+ saveIdTokenToLocalStorage(token: IdTokenPayload | null) {
63
+ const raw = token ? JSON.stringify(token) : null;
64
+ if (raw === null) {
65
+ localStorage.removeItem('id-token');
66
+ return;
40
67
  }
68
+ localStorage.setItem('id-token', raw);
69
+ }
70
+
71
+ addIdTokenChangedHandler(handler: () => any) {
72
+ this.getTokenManager().then((m) => m.addIdTokenChangedHandler(handler));
73
+ }
74
+
75
+ private getServerInfoInternal(): Promise<ServerOAuthInfoPayload> {
76
+ return this.insecureClient.getJson('status/info');
77
+ }
78
+
79
+ getServerInfo(): Promise<ServerOAuthInfoPayload> {
80
+ return this.serverInfo.get();
81
+ }
82
+
83
+ private getTokenManagerInternal(): Promise<OAuthTokenManager> {
41
84
  return this.getServerInfo()
42
- .then((info) => new OAuthTokenManager(info.oauthServerUrl, info.targetAudience));
85
+ .then(
86
+ (info) => {
87
+ const tm = new OAuthTokenManager(info.oauthServerUrl, info.targetAudience);
88
+ tm.addIdTokenChangedHandler((t: IdTokenPayload) => this.saveIdTokenToLocalStorage(t));
89
+ return tm;
90
+ }
91
+ );
92
+ }
93
+
94
+ getTokenManager(): Promise<OAuthTokenManager> {
95
+ return this.tokenManager.get();
43
96
  }
44
97
 
45
98
  login(login: string, password: string): Promise<boolean> {
@@ -52,9 +105,14 @@ export class RestClientWithOAuth extends RestClient {
52
105
  .then(() => true)
53
106
  }
54
107
 
55
- getHeaders(): Promise<RestClientHeaders> {
108
+ setIdTokenRaw(token: string): Promise<boolean> {
109
+ return this.getTokenManager()
110
+ .then(m => m.verifyIdToken(token))
111
+ }
112
+
113
+ getHeaders(url: string): Promise<RestClientHeaders> {
56
114
  return this.getTokenManager()
57
- .then(sm => sm.getAccessToken())
115
+ .then(tm => tm.getAccessToken(this.getPrivilege(url)))
58
116
  .then(
59
117
  (accessToken) => {
60
118
  return {