@hxa-rn/rnaa 8.1.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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/harmony/rnaa/LICENSE +21 -0
  4. package/harmony/rnaa/NOTICE +33 -0
  5. package/harmony/rnaa/OAT.xml +38 -0
  6. package/harmony/rnaa/build-profile.json5 +19 -0
  7. package/harmony/rnaa/hvigorfile.ts +2 -0
  8. package/harmony/rnaa/index.ets +1 -0
  9. package/harmony/rnaa/oh-package.json5 +14 -0
  10. package/harmony/rnaa/src/main/cpp/CMakeLists.txt +15 -0
  11. package/harmony/rnaa/src/main/cpp/RNAppAuthPackage.h +13 -0
  12. package/harmony/rnaa/src/main/cpp/generated/RNOH/generated/BaseRnaaPackage.h +72 -0
  13. package/harmony/rnaa/src/main/cpp/generated/RNOH/generated/turbo_modules/RNAppAuth.cpp +22 -0
  14. package/harmony/rnaa/src/main/cpp/generated/RNOH/generated/turbo_modules/RNAppAuth.h +16 -0
  15. package/harmony/rnaa/src/main/ets/DateUtil.ets +28 -0
  16. package/harmony/rnaa/src/main/ets/OAuthHttpClient.ets +293 -0
  17. package/harmony/rnaa/src/main/ets/OAuthProtocol.ets +293 -0
  18. package/harmony/rnaa/src/main/ets/PKCE.ets +105 -0
  19. package/harmony/rnaa/src/main/ets/RNAppAuthTurboModule.ets +713 -0
  20. package/harmony/rnaa/src/main/ets/RNAppAuthTurboModulesFactory.ets +18 -0
  21. package/harmony/rnaa/src/main/ets/Types.ets +98 -0
  22. package/harmony/rnaa/src/main/ets/generated/index.ets +5 -0
  23. package/harmony/rnaa/src/main/ets/generated/ts.ts +5 -0
  24. package/harmony/rnaa/src/main/ets/generated/turboModules/RNAppAuth.ts +38 -0
  25. package/harmony/rnaa/src/main/ets/generated/turboModules/ts.ts +5 -0
  26. package/harmony/rnaa/src/main/module.json5 +16 -0
  27. package/harmony/rnaa/src/main/resources/base/element/string.json +8 -0
  28. package/harmony/rnaa/src/main/resources/en_US/element/string.json +8 -0
  29. package/harmony/rnaa/src/main/resources/zh_CN/element/string.json +8 -0
  30. package/harmony/rnaa.har +0 -0
  31. package/package.json +69 -0
  32. package/src/index.d.ts +202 -0
  33. package/src/index.js +596 -0
  34. package/src/specs/v1/.gitkeep +1 -0
  35. package/src/specs/v1/NativeRNAppAuth.ts +138 -0
  36. package/src/specs/v2/.gitkeep +1 -0
@@ -0,0 +1,713 @@
1
+ /**
2
+ * RNAppAuth TurboModule (HarmonyOS / RN for OpenHarmony).
3
+ *
4
+ * Self-contained OAuth 2.0 / OpenID Connect bridge replacing the Android
5
+ * AppAuth-based RNAppAuthModule. Implements the 5 public methods
6
+ * (prefetchConfiguration / register / authorize / refresh / logout) plus the
7
+ * internal handleRedirect that completes a pending authorize/logout flow after
8
+ * the system browser redirects back through the RNOH 'url' device event.
9
+ *
10
+ * Protocol stack is split into OAuthProtocol.ets (URLs/parsing),
11
+ * OAuthHttpClient.ets (@ohos.net.http), PKCE.ets (cryptoFramework + Base64URL)
12
+ * and DateUtil.ets.
13
+ */
14
+ import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony/ts';
15
+ import { TM } from './generated/ts';
16
+ import hilog from '@ohos.hilog';
17
+ import { common, Want } from '@kit.AbilityKit';
18
+ import { util } from '@kit.ArkTS';
19
+ import { OAuthHttpClient } from './OAuthHttpClient';
20
+ import { OAuthProtocol, AuthorizationUrlParams, EndSessionUrlParams } from './OAuthProtocol';
21
+ import { PKCE } from './PKCE';
22
+ import { DateUtil } from './DateUtil';
23
+ import { Deferred, OAuthError, AuthorizationResponse } from './Types';
24
+
25
+ const DOMAIN: number = 0x0000;
26
+ const TAG: string = 'RNAppAuth';
27
+ const DEFAULT_TIMEOUT_MILLIS: number = 15000;
28
+
29
+ export class RNAppAuthTurboModule extends UITurboModule implements TM.RNAppAuth.Spec {
30
+ private httpClient: OAuthHttpClient = new OAuthHttpClient();
31
+
32
+ private serviceConfigCache: Map<string, TM.RNAppAuth.ServiceConfiguration> = new Map();
33
+
34
+ // Pending authorize state (bridged to handleRedirect).
35
+ private pendingAuthorize: Deferred<Object> | null = null;
36
+ // Pending logout state (bridged to handleRedirect).
37
+ private pendingLogout: Deferred<Object> | null = null;
38
+ private pendingFlowState: string = '';
39
+ private pendingCodeVerifier: string = '';
40
+ private pendingSkipCodeExchange: boolean = false;
41
+ private pendingUsePKCE: boolean = false;
42
+ private pendingClientSecret: string = '';
43
+ private pendingClientAuthMethod: string = 'basic';
44
+ private pendingAdditionalParameters: Record<string, string> = {};
45
+ private pendingRedirectUrl: string = '';
46
+ private pendingClientId: string = '';
47
+ private pendingScopes: string[] = [];
48
+ private pendingTimeoutMillis: number = DEFAULT_TIMEOUT_MILLIS;
49
+ private pendingTokenHeaders: Record<string, string> = {};
50
+ private pendingAuthorizationHeaders: Record<string, string> = {};
51
+ private pendingRegistrationHeaders: Record<string, string> = {};
52
+ private pendingServiceConfiguration: TM.RNAppAuth.ServiceConfiguration | null = null;
53
+ private pendingIssuer: string = '';
54
+ private pendingPostLogoutRedirectUri: string = '';
55
+ private pendingIdTokenHint: string = '';
56
+
57
+ constructor(ctx: UITurboModuleContext) {
58
+ super(ctx);
59
+ }
60
+
61
+ prefetchConfiguration(warmAndPrefetchChrome: boolean, issuer: string, redirectUrl: string,
62
+ clientId: string, scopes: string[], serviceConfiguration: TM.RNAppAuth.ServiceConfiguration,
63
+ dangerouslyAllowInsecureHttpRequests: boolean, customHeaders: TM.RNAppAuth.CustomHeaders,
64
+ connectionTimeoutMillis: number): Promise<void> {
65
+ // Async wrapper so a caught error becomes a rejected Promise (no throw).
66
+ return this.doPrefetchConfiguration(issuer, serviceConfiguration, connectionTimeoutMillis,
67
+ customHeaders);
68
+ }
69
+
70
+ private async doPrefetchConfiguration(issuer: string,
71
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration, connectionTimeoutMillis: number,
72
+ customHeaders: TM.RNAppAuth.CustomHeaders): Promise<void> {
73
+ try {
74
+ let timeout: number = RNAppAuthTurboModule.safeTimeout(connectionTimeoutMillis);
75
+ this.parseHeaderMap(customHeaders);
76
+ // warmAndPrefetchChrome is a no-op on Harmony (no Chrome Custom Tabs).
77
+ if (issuer !== null && issuer.length > 0 && this.serviceConfigCache.has(issuer)) {
78
+ return;
79
+ }
80
+ await this.resolveServiceConfiguration(issuer, serviceConfiguration, timeout);
81
+ } catch (err) {
82
+ throw RNAppAuthTurboModule.toOAuthError(err, 'service_configuration_fetch_error');
83
+ }
84
+ }
85
+
86
+ register(issuer: string, redirectUrls: string[], responseTypes: string[], grantTypes: string[],
87
+ subjectType: string, tokenEndpointAuthMethod: string, additionalParameters: Object,
88
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration, connectionTimeoutMillis: number,
89
+ dangerouslyAllowInsecureHttpRequests: boolean,
90
+ customHeaders: TM.RNAppAuth.CustomHeaders): Promise<TM.RNAppAuth.RegistrationResponse> {
91
+ return this.doRegister(issuer, redirectUrls, responseTypes, grantTypes, subjectType,
92
+ tokenEndpointAuthMethod, additionalParameters, serviceConfiguration, connectionTimeoutMillis,
93
+ customHeaders);
94
+ }
95
+
96
+ private async doRegister(issuer: string, redirectUrls: string[], responseTypes: string[],
97
+ grantTypes: string[], subjectType: string, tokenEndpointAuthMethod: string,
98
+ additionalParameters: Object, serviceConfiguration: TM.RNAppAuth.ServiceConfiguration,
99
+ connectionTimeoutMillis: number,
100
+ customHeaders: TM.RNAppAuth.CustomHeaders): Promise<TM.RNAppAuth.RegistrationResponse> {
101
+ try {
102
+ let timeout: number = RNAppAuthTurboModule.safeTimeout(connectionTimeoutMillis);
103
+ this.parseHeaderMap(customHeaders);
104
+ let config: TM.RNAppAuth.ServiceConfiguration =
105
+ await this.resolveServiceConfiguration(issuer, serviceConfiguration, timeout);
106
+ let registrationEndpoint: string | undefined = config.registrationEndpoint;
107
+ if (!RNAppAuthTurboModule.isNonEmptyString(registrationEndpoint)) {
108
+ throw new OAuthError('registration_failed', 'No registration endpoint available');
109
+ }
110
+ let endpoint: string = registrationEndpoint as string;
111
+ let additionalParams: Record<string, string> = RNAppAuthTurboModule.toStringMap(additionalParameters);
112
+ let body: Record<string, Object> = OAuthProtocol.buildRegistrationBody(redirectUrls,
113
+ responseTypes, grantTypes, subjectType, tokenEndpointAuthMethod, additionalParams);
114
+ let responseBody: Record<string, Object> = await this.httpClient.postJson(endpoint,
115
+ body, this.pendingRegistrationHeaders, timeout);
116
+ if (responseBody === null || responseBody === undefined) {
117
+ throw new OAuthError('registration_failed', 'Empty registration response');
118
+ }
119
+ return this.buildRegistrationResultMap(responseBody);
120
+ } catch (err) {
121
+ return Promise.reject(RNAppAuthTurboModule.toOAuthError(err, 'registration_failed'));
122
+ }
123
+ }
124
+
125
+ authorize(issuer: string, redirectUrl: string, clientId: string, clientSecret: string,
126
+ scopes: string[], additionalParameters: Object,
127
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration, skipCodeExchange: boolean,
128
+ connectionTimeoutMillis: number, useNonce: boolean, usePKCE: boolean,
129
+ clientAuthMethod: string, dangerouslyAllowInsecureHttpRequests: boolean,
130
+ customHeaders: TM.RNAppAuth.CustomHeaders): Promise<TM.RNAppAuth.AuthorizeResult> {
131
+ return this.doAuthorize(issuer, redirectUrl, clientId, clientSecret, scopes,
132
+ additionalParameters, serviceConfiguration, skipCodeExchange, connectionTimeoutMillis,
133
+ useNonce, usePKCE, clientAuthMethod, customHeaders);
134
+ }
135
+
136
+ private async doAuthorize(issuer: string, redirectUrl: string, clientId: string,
137
+ clientSecret: string, scopes: string[], additionalParameters: Object,
138
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration, skipCodeExchange: boolean,
139
+ connectionTimeoutMillis: number, useNonce: boolean, usePKCE: boolean,
140
+ clientAuthMethod: string, customHeaders: TM.RNAppAuth.CustomHeaders): Promise<TM.RNAppAuth.AuthorizeResult> {
141
+ try {
142
+ let timeout: number = RNAppAuthTurboModule.safeTimeout(connectionTimeoutMillis);
143
+ this.parseHeaderMap(customHeaders);
144
+ let config: TM.RNAppAuth.ServiceConfiguration =
145
+ await this.resolveServiceConfiguration(issuer, serviceConfiguration, timeout);
146
+ let authorizationEndpoint: string | undefined = config.authorizationEndpoint;
147
+ if (authorizationEndpoint === undefined || authorizationEndpoint.length === 0) {
148
+ throw new OAuthError('authentication_failed', 'No authorization endpoint available');
149
+ }
150
+
151
+ let rawParams: Record<string, string> = RNAppAuthTurboModule.toStringMap(additionalParameters);
152
+ let state: string = '';
153
+ let nonce: string = '';
154
+ let additionalParams: Record<string, string> = {};
155
+ let rawKeys: string[] = Object.keys(rawParams);
156
+ for (let key of rawKeys) {
157
+ if (key === 'state') {
158
+ state = rawParams[key];
159
+ } else if (key === 'nonce') {
160
+ nonce = rawParams[key];
161
+ } else {
162
+ additionalParams[key] = rawParams[key];
163
+ }
164
+ }
165
+ if (state.length === 0) {
166
+ state = PKCE.generateState();
167
+ }
168
+ if (nonce.length === 0 && useNonce) {
169
+ nonce = PKCE.generateNonce();
170
+ }
171
+
172
+ let codeVerifier: string = '';
173
+ let codeChallenge: string = '';
174
+ if (usePKCE) {
175
+ codeVerifier = PKCE.generateCodeVerifier();
176
+ codeChallenge = await PKCE.deriveCodeChallenge(codeVerifier);
177
+ }
178
+
179
+ let urlParams: AuthorizationUrlParams = {
180
+ authorizationEndpoint: authorizationEndpoint,
181
+ clientId: clientId,
182
+ redirectUrl: redirectUrl,
183
+ scopes: scopes,
184
+ state: state,
185
+ nonce: nonce,
186
+ codeChallenge: codeChallenge,
187
+ additionalParameters: additionalParams,
188
+ };
189
+ let authUrl: string = OAuthProtocol.buildAuthorizationUrl(urlParams);
190
+
191
+ // Store pending state before opening the browser.
192
+ this.pendingFlowState = state;
193
+ this.pendingCodeVerifier = codeVerifier;
194
+ this.pendingSkipCodeExchange = skipCodeExchange;
195
+ this.pendingUsePKCE = usePKCE;
196
+ this.pendingClientSecret = clientSecret === null ? '' : clientSecret;
197
+ this.pendingClientAuthMethod = clientAuthMethod === null ? 'basic' : clientAuthMethod;
198
+ this.pendingAdditionalParameters = additionalParams;
199
+ this.pendingRedirectUrl = redirectUrl;
200
+ this.pendingClientId = clientId;
201
+ this.pendingScopes = scopes;
202
+ this.pendingTimeoutMillis = timeout;
203
+ this.pendingServiceConfiguration = config;
204
+ this.pendingIssuer = issuer === null ? '' : issuer;
205
+
206
+ let deferred: Deferred<Object> = new Deferred<Object>();
207
+ this.pendingAuthorize = deferred;
208
+
209
+ await this.openBrowser(authUrl);
210
+ return deferred.promise as Promise<TM.RNAppAuth.AuthorizeResult>;
211
+ } catch (err) {
212
+ return Promise.reject(RNAppAuthTurboModule.toOAuthError(err, 'authentication_failed'));
213
+ }
214
+ }
215
+
216
+ refresh(issuer: string, redirectUrl: string, clientId: string, clientSecret: string,
217
+ refreshToken: string, scopes: string[], additionalParameters: Object,
218
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration, connectionTimeoutMillis: number,
219
+ clientAuthMethod: string, dangerouslyAllowInsecureHttpRequests: boolean,
220
+ customHeaders: TM.RNAppAuth.CustomHeaders): Promise<TM.RNAppAuth.RefreshResult> {
221
+ return this.doRefresh(issuer, redirectUrl, clientId, clientSecret, refreshToken, scopes,
222
+ additionalParameters, serviceConfiguration, connectionTimeoutMillis, clientAuthMethod,
223
+ customHeaders);
224
+ }
225
+
226
+ private async doRefresh(issuer: string, redirectUrl: string, clientId: string,
227
+ clientSecret: string, refreshToken: string, scopes: string[],
228
+ additionalParameters: Object, serviceConfiguration: TM.RNAppAuth.ServiceConfiguration,
229
+ connectionTimeoutMillis: number, clientAuthMethod: string,
230
+ customHeaders: TM.RNAppAuth.CustomHeaders): Promise<TM.RNAppAuth.RefreshResult> {
231
+ try {
232
+ let timeout: number = RNAppAuthTurboModule.safeTimeout(connectionTimeoutMillis);
233
+ this.parseHeaderMap(customHeaders);
234
+ let config: TM.RNAppAuth.ServiceConfiguration =
235
+ await this.resolveServiceConfiguration(issuer, serviceConfiguration, timeout);
236
+ let tokenEndpoint: string | undefined = config.tokenEndpoint;
237
+ if (tokenEndpoint === undefined || tokenEndpoint.length === 0) {
238
+ throw new OAuthError('token_refresh_failed', 'No token endpoint available');
239
+ }
240
+ let additionalParams: Record<string, string> = RNAppAuthTurboModule.toStringMap(additionalParameters);
241
+ if (clientSecret !== null && clientSecret.length > 0) {
242
+ additionalParams.client_secret = clientSecret;
243
+ }
244
+ let params: Record<string, string> = OAuthProtocol.buildRefreshTokenParams(refreshToken,
245
+ clientId, scopes, redirectUrl, additionalParams);
246
+ let headers: Record<string, string> = RNAppAuthTurboModule.copyMap(this.pendingTokenHeaders);
247
+ if (clientSecret !== null && clientSecret.length > 0 && clientAuthMethod !== 'post') {
248
+ headers.Authorization = RNAppAuthTurboModule.buildBasicAuthHeader(clientId, clientSecret);
249
+ }
250
+ let responseBody: Record<string, Object> = await this.httpClient.postForm(tokenEndpoint, params,
251
+ headers, timeout);
252
+ return this.buildRefreshResultMap(responseBody);
253
+ } catch (err) {
254
+ return Promise.reject(RNAppAuthTurboModule.toOAuthError(err, 'token_refresh_failed'));
255
+ }
256
+ }
257
+
258
+ logout(issuer: string, idTokenHint: string, postLogoutRedirectUri: string,
259
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration, additionalParameters: Object,
260
+ dangerouslyAllowInsecureHttpRequests: boolean): Promise<TM.RNAppAuth.EndSessionResult> {
261
+ return this.doLogout(issuer, idTokenHint, postLogoutRedirectUri, serviceConfiguration,
262
+ additionalParameters);
263
+ }
264
+
265
+ private async doLogout(issuer: string, idTokenHint: string, postLogoutRedirectUri: string,
266
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration,
267
+ additionalParameters: Object): Promise<TM.RNAppAuth.EndSessionResult> {
268
+ try {
269
+ let timeout: number = DEFAULT_TIMEOUT_MILLIS;
270
+ let config: TM.RNAppAuth.ServiceConfiguration =
271
+ await this.resolveServiceConfiguration(issuer, serviceConfiguration, timeout);
272
+ let endSessionEndpoint: string | undefined = config.endSessionEndpoint;
273
+ if (endSessionEndpoint === undefined || endSessionEndpoint.length === 0) {
274
+ throw new OAuthError('end_session_failed', 'No end session endpoint available');
275
+ }
276
+ let rawLogoutParams: Record<string, string> = RNAppAuthTurboModule.toStringMap(additionalParameters);
277
+ let state: string = '';
278
+ let additionalParams: Record<string, string> = {};
279
+ let logoutKeys: string[] = Object.keys(rawLogoutParams);
280
+ for (let key of logoutKeys) {
281
+ if (key === 'state') {
282
+ state = rawLogoutParams[key];
283
+ } else {
284
+ additionalParams[key] = rawLogoutParams[key];
285
+ }
286
+ }
287
+ if (state.length === 0) {
288
+ state = PKCE.generateState();
289
+ }
290
+ let urlParams: EndSessionUrlParams = {
291
+ endSessionEndpoint: endSessionEndpoint,
292
+ idTokenHint: idTokenHint,
293
+ postLogoutRedirectUri: postLogoutRedirectUri,
294
+ state: state,
295
+ additionalParameters: additionalParams,
296
+ };
297
+ let endSessionUrl: string = OAuthProtocol.buildEndSessionUrl(urlParams);
298
+
299
+ this.pendingFlowState = state;
300
+ this.pendingPostLogoutRedirectUri = postLogoutRedirectUri;
301
+ this.pendingIdTokenHint = idTokenHint;
302
+
303
+ let deferred: Deferred<Object> = new Deferred<Object>();
304
+ this.pendingLogout = deferred;
305
+
306
+ await this.openBrowser(endSessionUrl);
307
+ return deferred.promise as Promise<TM.RNAppAuth.EndSessionResult>;
308
+ } catch (err) {
309
+ return Promise.reject(RNAppAuthTurboModule.toOAuthError(err, 'end_session_failed'));
310
+ }
311
+ }
312
+
313
+ handleRedirect(url: string): Promise<boolean> {
314
+ return this.doHandleRedirect(url);
315
+ }
316
+
317
+ cancelPendingFlow(): Promise<boolean> {
318
+ let cancelled: boolean = false;
319
+ if (this.pendingAuthorize !== null) {
320
+ let deferred: Deferred<Object> = this.pendingAuthorize;
321
+ this.pendingAuthorize = null;
322
+ deferred.reject(new OAuthError('access_denied', 'Authorization flow cancelled by user'));
323
+ cancelled = true;
324
+ }
325
+ if (this.pendingLogout !== null) {
326
+ let deferred: Deferred<Object> = this.pendingLogout;
327
+ this.pendingLogout = null;
328
+ deferred.reject(new OAuthError('access_denied', 'End session flow cancelled by user'));
329
+ cancelled = true;
330
+ }
331
+ return Promise.resolve(cancelled);
332
+ }
333
+
334
+ private async doHandleRedirect(url: string): Promise<boolean> {
335
+ let response: AuthorizationResponse = OAuthProtocol.parseCallbackUrl(url);
336
+ // A redirect is only ours when it carries OAuth callback parameters
337
+ // (state/code/error). This avoids consuming the app's own unrelated deep
338
+ // links that also flow through the RNOH 'url' device event.
339
+ let isOAuthCallback: boolean = response.state.length > 0 ||
340
+ response.authorizationCode.length > 0 || response.error.length > 0;
341
+ if (!isOAuthCallback) {
342
+ return false;
343
+ }
344
+ // Only handle redirects whose state matches an in-flight flow.
345
+ if (response.state.length > 0 && response.state !== this.pendingFlowState) {
346
+ return false;
347
+ }
348
+
349
+ if (this.pendingAuthorize !== null) {
350
+ let deferred: Deferred<Object> = this.pendingAuthorize;
351
+ this.pendingAuthorize = null;
352
+ return await this.completeAuthorize(deferred, response);
353
+ }
354
+
355
+ if (this.pendingLogout !== null) {
356
+ let deferred: Deferred<Object> = this.pendingLogout;
357
+ this.pendingLogout = null;
358
+ let result: TM.RNAppAuth.EndSessionResult = {
359
+ idTokenHint: this.pendingIdTokenHint,
360
+ postLogoutRedirectUri: this.pendingPostLogoutRedirectUri,
361
+ state: response.state.length > 0 ? response.state : this.pendingFlowState,
362
+ };
363
+ deferred.resolve(result);
364
+ return true;
365
+ }
366
+
367
+ return false;
368
+ }
369
+
370
+ private async completeAuthorize(deferred: Deferred<Object>,
371
+ response: AuthorizationResponse): Promise<boolean> {
372
+ if (response.error.length > 0) {
373
+ deferred.reject(new OAuthError(response.error,
374
+ response.errorDescription.length > 0 ? response.errorDescription : response.error));
375
+ return true;
376
+ }
377
+ if (response.authorizationCode.length === 0) {
378
+ deferred.reject(new OAuthError('authentication_failed', 'No authorization code in redirect'));
379
+ return true;
380
+ }
381
+ if (this.pendingSkipCodeExchange) {
382
+ let result: TM.RNAppAuth.AuthorizeResult = this.buildAuthorizationResponseMap(response);
383
+ deferred.resolve(result);
384
+ return true;
385
+ }
386
+ try {
387
+ let tokenBody: Record<string, Object> = await this.exchangeCodeForToken(
388
+ response.authorizationCode);
389
+ let result: TM.RNAppAuth.AuthorizeResult = this.buildAuthorizeResultMap(tokenBody, response);
390
+ deferred.resolve(result);
391
+ return true;
392
+ } catch (err) {
393
+ deferred.reject(RNAppAuthTurboModule.toOAuthError(err, 'token_exchange_failed'));
394
+ return true;
395
+ }
396
+ }
397
+
398
+ private async exchangeCodeForToken(code: string): Promise<Record<string, Object>> {
399
+ let config: TM.RNAppAuth.ServiceConfiguration | null = this.pendingServiceConfiguration;
400
+ if (config === null || config.tokenEndpoint === undefined || config.tokenEndpoint.length === 0) {
401
+ throw new OAuthError('token_exchange_failed', 'No token endpoint available');
402
+ }
403
+ let params: Record<string, string> = OAuthProtocol.buildTokenExchangeParams(code,
404
+ this.pendingRedirectUrl, this.pendingClientId, this.pendingCodeVerifier,
405
+ this.pendingAdditionalParameters);
406
+ let headers: Record<string, string> = RNAppAuthTurboModule.copyMap(this.pendingTokenHeaders);
407
+ if (this.pendingClientSecret.length > 0) {
408
+ if (this.pendingClientAuthMethod === 'post') {
409
+ params.client_secret = this.pendingClientSecret;
410
+ } else {
411
+ headers.Authorization = RNAppAuthTurboModule.buildBasicAuthHeader(this.pendingClientId,
412
+ this.pendingClientSecret);
413
+ }
414
+ }
415
+ return await this.httpClient.postForm(config.tokenEndpoint, params, headers,
416
+ this.pendingTimeoutMillis);
417
+ }
418
+
419
+ private buildAuthorizationResponseMap(response: AuthorizationResponse): TM.RNAppAuth.AuthorizeResult {
420
+ let additionalParams: Object = RNAppAuthTurboModule.toJsObject(response.additionalParameters);
421
+ let result: TM.RNAppAuth.AuthorizeResult = {
422
+ accessToken: response.accessToken,
423
+ accessTokenExpirationDate: DateUtil.formatTimestamp(response.accessTokenExpirationTime),
424
+ authorizeAdditionalParameters: additionalParams,
425
+ idToken: response.idToken,
426
+ refreshToken: '',
427
+ tokenType: response.tokenType,
428
+ scopes: this.pendingScopes,
429
+ authorizationCode: response.authorizationCode,
430
+ };
431
+ if (response.accessTokenExpirationTime > 0) {
432
+ result.accessTokenExpirationDate = DateUtil.formatTimestamp(response.accessTokenExpirationTime);
433
+ }
434
+ if (this.pendingCodeVerifier.length > 0) {
435
+ result.codeVerifier = this.pendingCodeVerifier;
436
+ }
437
+ return result;
438
+ }
439
+
440
+ private buildAuthorizeResultMap(tokenBody: Record<string, Object>,
441
+ response: AuthorizationResponse): TM.RNAppAuth.AuthorizeResult {
442
+ let scopes: string[] = this.pendingScopes;
443
+ let scopeStr: string = OAuthHttpClient.asString(tokenBody, 'scope');
444
+ if (scopeStr.length > 0) {
445
+ scopes = scopeStr.split(' ');
446
+ } else if (response.scope.length > 0) {
447
+ scopes = response.scope.split(' ');
448
+ }
449
+ let result: TM.RNAppAuth.AuthorizeResult = {
450
+ accessToken: OAuthHttpClient.asString(tokenBody, 'access_token'),
451
+ accessTokenExpirationDate: '',
452
+ authorizeAdditionalParameters: RNAppAuthTurboModule.toJsObject(response.additionalParameters),
453
+ tokenAdditionalParameters: RNAppAuthTurboModule.toJsObject(
454
+ RNAppAuthTurboModule.remainingParams(tokenBody, RNAppAuthTurboModule.TOKEN_STANDARD_KEYS)),
455
+ idToken: OAuthHttpClient.asString(tokenBody, 'id_token'),
456
+ refreshToken: OAuthHttpClient.asString(tokenBody, 'refresh_token'),
457
+ tokenType: OAuthHttpClient.asString(tokenBody, 'token_type'),
458
+ scopes: scopes,
459
+ authorizationCode: response.authorizationCode,
460
+ };
461
+ let expiresIn: number = OAuthHttpClient.asNumber(tokenBody, 'expires_in');
462
+ if (expiresIn > 0) {
463
+ result.accessTokenExpirationDate = DateUtil.formatTimestamp(Date.now() + expiresIn * 1000);
464
+ }
465
+ if (this.pendingCodeVerifier.length > 0) {
466
+ result.codeVerifier = this.pendingCodeVerifier;
467
+ }
468
+ return result;
469
+ }
470
+
471
+ private buildRefreshResultMap(body: Record<string, Object>): TM.RNAppAuth.RefreshResult {
472
+ let result: TM.RNAppAuth.RefreshResult = {
473
+ accessToken: OAuthHttpClient.asString(body, 'access_token'),
474
+ accessTokenExpirationDate: '',
475
+ additionalParameters: RNAppAuthTurboModule.toJsObject(
476
+ RNAppAuthTurboModule.remainingParams(body, RNAppAuthTurboModule.TOKEN_STANDARD_KEYS)),
477
+ idToken: OAuthHttpClient.asString(body, 'id_token'),
478
+ refreshToken: OAuthHttpClient.asString(body, 'refresh_token'),
479
+ tokenType: OAuthHttpClient.asString(body, 'token_type'),
480
+ };
481
+ let expiresIn: number = OAuthHttpClient.asNumber(body, 'expires_in');
482
+ if (expiresIn > 0) {
483
+ result.accessTokenExpirationDate = DateUtil.formatTimestamp(Date.now() + expiresIn * 1000);
484
+ }
485
+ return result;
486
+ }
487
+
488
+ private buildRegistrationResultMap(body: Record<string, Object>): TM.RNAppAuth.RegistrationResponse {
489
+ let result: TM.RNAppAuth.RegistrationResponse = {
490
+ clientId: OAuthHttpClient.asString(body, 'client_id'),
491
+ additionalParameters: RNAppAuthTurboModule.toJsObject(
492
+ RNAppAuthTurboModule.remainingParams(body, RNAppAuthTurboModule.REGISTRATION_STANDARD_KEYS)),
493
+ };
494
+ let clientIdIssuedAt: number = OAuthHttpClient.asNumber(body, 'client_id_issued_at');
495
+ if (clientIdIssuedAt > 0) {
496
+ result.clientIdIssuedAt = DateUtil.formatTimestamp(clientIdIssuedAt * 1000);
497
+ }
498
+ let clientSecretExpiresAt: number = OAuthHttpClient.asNumber(body, 'client_secret_expires_at');
499
+ if (clientSecretExpiresAt > 0) {
500
+ result.clientSecretExpiresAt = DateUtil.formatTimestamp(clientSecretExpiresAt * 1000);
501
+ }
502
+ let clientSecret: string = OAuthHttpClient.asString(body, 'client_secret');
503
+ if (clientSecret.length > 0) {
504
+ result.clientSecret = clientSecret;
505
+ }
506
+ let registrationAccessToken: string = OAuthHttpClient.asString(body, 'registration_access_token');
507
+ if (registrationAccessToken.length > 0) {
508
+ result.registrationAccessToken = registrationAccessToken;
509
+ }
510
+ let registrationClientUri: string = OAuthHttpClient.asString(body, 'registration_client_uri');
511
+ if (registrationClientUri.length > 0) {
512
+ result.registrationClientUri = registrationClientUri;
513
+ }
514
+ let tokenEndpointAuthMethod: string = OAuthHttpClient.asString(body, 'token_endpoint_auth_method');
515
+ if (tokenEndpointAuthMethod.length > 0) {
516
+ result.tokenEndpointAuthMethod = tokenEndpointAuthMethod;
517
+ }
518
+ return result;
519
+ }
520
+
521
+ private async resolveServiceConfiguration(issuer: string,
522
+ serviceConfiguration: TM.RNAppAuth.ServiceConfiguration,
523
+ timeoutMillis: number): Promise<TM.RNAppAuth.ServiceConfiguration> {
524
+ if (serviceConfiguration !== null && serviceConfiguration !== undefined &&
525
+ RNAppAuthTurboModule.isNonEmptyString(serviceConfiguration.authorizationEndpoint)) {
526
+ return serviceConfiguration;
527
+ }
528
+ if (serviceConfiguration !== null && serviceConfiguration !== undefined &&
529
+ RNAppAuthTurboModule.isNonEmptyString(serviceConfiguration.registrationEndpoint)) {
530
+ return serviceConfiguration;
531
+ }
532
+ if (issuer !== null && issuer.length > 0 && this.serviceConfigCache.has(issuer)) {
533
+ return RNAppAuthTurboModule.mergeServiceConfigurationOverrides(
534
+ this.serviceConfigCache.get(issuer)!, serviceConfiguration);
535
+ }
536
+ if (issuer === null || issuer.length === 0) {
537
+ throw new OAuthError('service_configuration_fetch_error',
538
+ 'No issuer or serviceConfiguration provided');
539
+ }
540
+ let discovery = await this.httpClient.fetchDiscovery(issuer, timeoutMillis);
541
+ let config: TM.RNAppAuth.ServiceConfiguration = {
542
+ authorizationEndpoint: discovery.authorizationEndpoint,
543
+ tokenEndpoint: discovery.tokenEndpoint,
544
+ revocationEndpoint: discovery.revocationEndpoint.length > 0 ? discovery.revocationEndpoint : undefined,
545
+ registrationEndpoint: discovery.registrationEndpoint.length > 0 ? discovery.registrationEndpoint : undefined,
546
+ endSessionEndpoint: discovery.endSessionEndpoint.length > 0 ? discovery.endSessionEndpoint : undefined,
547
+ };
548
+ config = RNAppAuthTurboModule.mergeServiceConfigurationOverrides(config, serviceConfiguration);
549
+ this.serviceConfigCache.set(issuer, config);
550
+ return config;
551
+ }
552
+
553
+ private static mergeServiceConfigurationOverrides(
554
+ base: TM.RNAppAuth.ServiceConfiguration,
555
+ overrides: TM.RNAppAuth.ServiceConfiguration): TM.RNAppAuth.ServiceConfiguration {
556
+ if (overrides === null || overrides === undefined) {
557
+ return base;
558
+ }
559
+ let merged: TM.RNAppAuth.ServiceConfiguration = {
560
+ authorizationEndpoint: base.authorizationEndpoint,
561
+ tokenEndpoint: base.tokenEndpoint,
562
+ revocationEndpoint: base.revocationEndpoint,
563
+ registrationEndpoint: base.registrationEndpoint,
564
+ endSessionEndpoint: base.endSessionEndpoint,
565
+ };
566
+ if (RNAppAuthTurboModule.isNonEmptyString(overrides.authorizationEndpoint)) {
567
+ merged.authorizationEndpoint = overrides.authorizationEndpoint;
568
+ }
569
+ if (RNAppAuthTurboModule.isNonEmptyString(overrides.tokenEndpoint)) {
570
+ merged.tokenEndpoint = overrides.tokenEndpoint;
571
+ }
572
+ if (RNAppAuthTurboModule.isNonEmptyString(overrides.revocationEndpoint)) {
573
+ merged.revocationEndpoint = overrides.revocationEndpoint;
574
+ }
575
+ if (RNAppAuthTurboModule.isNonEmptyString(overrides.registrationEndpoint)) {
576
+ merged.registrationEndpoint = overrides.registrationEndpoint;
577
+ }
578
+ if (RNAppAuthTurboModule.isNonEmptyString(overrides.endSessionEndpoint)) {
579
+ merged.endSessionEndpoint = overrides.endSessionEndpoint;
580
+ }
581
+ return merged;
582
+ }
583
+
584
+ private async openBrowser(url: string): Promise<void> {
585
+ let want: Want = {
586
+ action: 'ohos.want.action.viewData',
587
+ entities: ['entity.system.browsable'],
588
+ uri: url,
589
+ };
590
+ try {
591
+ let ctx: common.UIAbilityContext = this.ctx.uiAbilityContext;
592
+ await ctx.startAbility(want);
593
+ } catch (err) {
594
+ hilog.error(DOMAIN, TAG, `openBrowser failed: ${err}`);
595
+ throw new OAuthError('authentication_failed', 'Failed to launch browser for authorization');
596
+ }
597
+ }
598
+
599
+ private parseHeaderMap(headerMap: Object): void {
600
+ if (headerMap === null || headerMap === undefined) {
601
+ return;
602
+ }
603
+ let map = headerMap as Record<string, Object>;
604
+ let register = map.register;
605
+ let authorize = map.authorize;
606
+ let token = map.token;
607
+ if (register !== undefined && register !== null) {
608
+ this.pendingRegistrationHeaders = RNAppAuthTurboModule.toStringMap(register);
609
+ }
610
+ if (authorize !== undefined && authorize !== null) {
611
+ this.pendingAuthorizationHeaders = RNAppAuthTurboModule.toStringMap(authorize);
612
+ }
613
+ if (token !== undefined && token !== null) {
614
+ this.pendingTokenHeaders = RNAppAuthTurboModule.toStringMap(token);
615
+ }
616
+ }
617
+
618
+ private static readonly TOKEN_STANDARD_KEYS: string[] =
619
+ ['access_token', 'token_type', 'expires_in', 'refresh_token', 'id_token', 'scope'];
620
+ private static readonly REGISTRATION_STANDARD_KEYS: string[] =
621
+ ['client_id', 'client_secret', 'client_id_issued_at', 'client_secret_expires_at',
622
+ 'registration_access_token', 'registration_client_uri', 'token_endpoint_auth_method'];
623
+
624
+ private static safeTimeout(connectionTimeoutMillis: number): number {
625
+ if (connectionTimeoutMillis !== null && connectionTimeoutMillis > 0) {
626
+ return connectionTimeoutMillis;
627
+ }
628
+ return DEFAULT_TIMEOUT_MILLIS;
629
+ }
630
+
631
+ /**
632
+ * JS/TurboModule may pass null for omitted optional strings.
633
+ * Treat null the same as undefined/empty so `.length` never runs on null.
634
+ * Non-empty strings keep previous behavior (used by authorize/refresh too).
635
+ */
636
+ private static isNonEmptyString(value: string | null | undefined): boolean {
637
+ if (value === null || value === undefined) {
638
+ return false;
639
+ }
640
+ return value.length > 0;
641
+ }
642
+
643
+ private static toStringMap(obj: Object): Record<string, string> {
644
+ let result: Record<string, string> = {};
645
+ if (obj === null || obj === undefined) {
646
+ return result;
647
+ }
648
+ let rec = obj as Record<string, Object>;
649
+ let keys: string[] = Object.keys(rec);
650
+ for (let key of keys) {
651
+ let value: Object | undefined = rec[key];
652
+ result[key] = value === undefined || value === null ? '' : `${value}`;
653
+ }
654
+ return result;
655
+ }
656
+
657
+ private static copyMap(source: Record<string, string>): Record<string, string> {
658
+ let result: Record<string, string> = {};
659
+ let keys: string[] = Object.keys(source);
660
+ for (let key of keys) {
661
+ result[key] = source[key];
662
+ }
663
+ return result;
664
+ }
665
+
666
+ private static buildBasicAuthHeader(clientId: string, clientSecret: string): string {
667
+ try {
668
+ let credentials: string = `${clientId}:${clientSecret}`;
669
+ let bytes: Uint8Array = new util.TextEncoder().encodeInto(credentials);
670
+ let encoded: string = new util.Base64Helper().encodeToStringSync(bytes, util.Type.BASIC);
671
+ return `Basic ${encoded}`;
672
+ } catch (err) {
673
+ hilog.error(DOMAIN, TAG, `buildBasicAuthHeader failed: ${err}`);
674
+ return 'Basic ';
675
+ }
676
+ }
677
+
678
+ private static remainingParams(body: Record<string, Object>,
679
+ standardKeys: string[]): Record<string, string> {
680
+ let result: Record<string, string> = {};
681
+ let keys: string[] = Object.keys(body);
682
+ for (let key of keys) {
683
+ let isStandard: boolean = false;
684
+ for (let sk of standardKeys) {
685
+ if (sk === key) {
686
+ isStandard = true;
687
+ break;
688
+ }
689
+ }
690
+ if (!isStandard) {
691
+ let value: Object | undefined = body[key];
692
+ result[key] = value === undefined || value === null ? '' : `${value}`;
693
+ }
694
+ }
695
+ return result;
696
+ }
697
+
698
+ private static toJsObject(params: Record<string, string>): Object {
699
+ let result: Record<string, string> = {};
700
+ let keys: string[] = Object.keys(params);
701
+ for (let key of keys) {
702
+ result[key] = params[key];
703
+ }
704
+ return result;
705
+ }
706
+
707
+ private static toOAuthError(err: Object, fallbackCode: string): OAuthError {
708
+ if (err instanceof OAuthError) {
709
+ return err;
710
+ }
711
+ return new OAuthError(fallbackCode, OAuthHttpClient.errorMessage(err));
712
+ }
713
+ }