@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,293 @@
1
+ /**
2
+ * OAuth HTTP client built on @ohos.net.http.
3
+ *
4
+ * Replaces net.openid.appauth.AuthorizationServiceConfiguration.fetchFromUrl /
5
+ * AuthorizationService.performTokenRequest / performRegistrationRequest on
6
+ * HarmonyOS. Supports:
7
+ * - GET OpenID Connect discovery (.well-known/openid-configuration)
8
+ * - POST application/x-www-form-urlencoded (token / refresh)
9
+ * - POST application/json (dynamic client registration)
10
+ *
11
+ * connectionTimeoutMillis/readTimeout are passed through from the JS layer
12
+ * (already converted to milliseconds on the Harmony branch).
13
+ */
14
+ import http from '@ohos.net.http';
15
+ import { util } from '@kit.ArkTS';
16
+ import hilog from '@ohos.hilog';
17
+ import { DiscoveryDocument, OAuthError } from './Types';
18
+
19
+ const DOMAIN: number = 0x0000;
20
+ const TAG: string = 'RNAppAuth';
21
+ const HTTP_OK = 200;
22
+
23
+ interface NativeThrownError {
24
+ code: number;
25
+ message: string;
26
+ }
27
+
28
+ export class OAuthHttpClient {
29
+ /**
30
+ * Build a query string from a map of form parameters.
31
+ * Values are URL-encoded with encodeURIComponent (RFC 6749 §2.3.1).
32
+ */
33
+ static buildFormBody(parameters: Record<string, string>): string {
34
+ let parts: string[] = [];
35
+ let keys: string[] = Object.keys(parameters);
36
+ for (let key of keys) {
37
+ let value: string = parameters[key];
38
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
39
+ }
40
+ return parts.join('&');
41
+ }
42
+
43
+ /**
44
+ * Fetch and parse the OIDC discovery document for an issuer.
45
+ * Discovery URL = {issuer}/.well-known/openid-configuration
46
+ * (mirrors AuthorizationServiceConfiguration.WELL_KNOWN_PATH).
47
+ */
48
+ async fetchDiscovery(issuer: string, timeoutMillis: number): Promise<DiscoveryDocument> {
49
+ try {
50
+ let discoveryUrl: string = OAuthHttpClient.buildConfigurationUriFromIssuer(issuer);
51
+ let responseBody: Record<string, Object> = await this.requestJson(discoveryUrl,
52
+ http.RequestMethod.GET, '', {}, timeoutMillis, 'service_configuration_fetch_error');
53
+ let doc: DiscoveryDocument = {
54
+ authorizationEndpoint: OAuthHttpClient.asString(responseBody, 'authorization_endpoint'),
55
+ tokenEndpoint: OAuthHttpClient.asString(responseBody, 'token_endpoint'),
56
+ registrationEndpoint: OAuthHttpClient.asString(responseBody, 'registration_endpoint'),
57
+ revocationEndpoint: OAuthHttpClient.asString(responseBody, 'revocation_endpoint'),
58
+ endSessionEndpoint: OAuthHttpClient.asString(responseBody, 'end_session_endpoint'),
59
+ };
60
+ if (doc.authorizationEndpoint.length === 0 || doc.tokenEndpoint.length === 0) {
61
+ throw new OAuthError('service_configuration_fetch_error',
62
+ 'Invalid discovery document: missing authorization_endpoint or token_endpoint');
63
+ }
64
+ return doc;
65
+ } catch (err) {
66
+ if (err instanceof OAuthError && err.code === 'service_configuration_fetch_error') {
67
+ throw err;
68
+ }
69
+ throw new OAuthError('service_configuration_fetch_error', OAuthHttpClient.errorMessage(err));
70
+ }
71
+ }
72
+
73
+ /**
74
+ * POST application/x-www-form-urlencoded (token / refresh endpoints).
75
+ * On a non-2xx response, parses the OAuth error code/description and rejects
76
+ * with the standard OAuth error code.
77
+ */
78
+ async postForm(url: string, formParameters: Record<string, string>,
79
+ headers: Record<string, string>, timeoutMillis: number): Promise<Record<string, Object>> {
80
+ let body: string = OAuthHttpClient.buildFormBody(formParameters);
81
+ let formHeaders: Record<string, string> = {
82
+ 'Content-Type': 'application/x-www-form-urlencoded',
83
+ };
84
+ let mergedHeaders: Record<string, string> = OAuthHttpClient.mergeHeaders(formHeaders, headers);
85
+ return await this.requestJson(url, http.RequestMethod.POST, body, mergedHeaders, timeoutMillis);
86
+ }
87
+
88
+ /**
89
+ * POST application/json (registration endpoint).
90
+ */
91
+ async postJson(url: string, bodyObject: Object, headers: Record<string, string>,
92
+ timeoutMillis: number): Promise<Record<string, Object>> {
93
+ let body: string = JSON.stringify(bodyObject);
94
+ let jsonHeaders: Record<string, string> = {
95
+ 'Content-Type': 'application/json',
96
+ };
97
+ let mergedHeaders: Record<string, string> = OAuthHttpClient.mergeHeaders(jsonHeaders, headers);
98
+ return await this.requestJson(url, http.RequestMethod.POST, body, mergedHeaders, timeoutMillis);
99
+ }
100
+
101
+ /**
102
+ * GET request with no body (used for discovery).
103
+ */
104
+ private async requestJson(url: string, method: http.RequestMethod, body: string,
105
+ headers: Record<string, string>, timeoutMillis: number,
106
+ fallbackCode: string = 'run_time_exception'): Promise<Record<string, Object>> {
107
+ let httpRequest: http.HttpRequest | null = null;
108
+ try {
109
+ httpRequest = http.createHttp();
110
+ let options: http.HttpRequestOptions = {
111
+ method: method,
112
+ header: headers,
113
+ connectTimeout: timeoutMillis,
114
+ readTimeout: timeoutMillis,
115
+ };
116
+ if (body.length > 0) {
117
+ options.extraData = body;
118
+ }
119
+ let response: http.HttpResponse = await httpRequest.request(url, options);
120
+ let code: number = response.responseCode;
121
+ let resultText: string = OAuthHttpClient.asResponseText(response.result);
122
+ if (code < HTTP_OK || code >= 300) {
123
+ throw OAuthHttpClient.buildHttpError(code, resultText);
124
+ }
125
+ return OAuthHttpClient.parseJsonObject(resultText);
126
+ } catch (err) {
127
+ hilog.error(DOMAIN, TAG, `requestJson failed for ${url}: ${OAuthHttpClient.errorMessage(err)}`);
128
+ if (err instanceof OAuthError) {
129
+ throw err;
130
+ }
131
+ throw new OAuthError(fallbackCode, OAuthHttpClient.errorMessage(err));
132
+ } finally {
133
+ if (httpRequest !== null) {
134
+ httpRequest.destroy();
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Derive the discovery URL for an issuer, mirroring the Android
141
+ * AuthorizationServiceConfiguration path construction:
142
+ * {issuer}/.well-known/openid-configuration
143
+ */
144
+ static buildConfigurationUriFromIssuer(issuer: string): string {
145
+ let base: string = issuer;
146
+ while (base.length > 0 && base.endsWith('/')) {
147
+ base = base.substring(0, base.length - 1);
148
+ }
149
+ return `${base}/.well-known/openid-configuration`;
150
+ }
151
+
152
+ /**
153
+ * Map a non-2xx HTTP response to an OAuthError with the standard OAuth
154
+ * error code from the JSON body when present.
155
+ */
156
+ private static buildHttpError(code: number, resultText: string): OAuthError {
157
+ let errorCode: string = 'run_time_exception';
158
+ let errorDescription: string = `HTTP ${code}`;
159
+ if (resultText.length > 0) {
160
+ try {
161
+ let body: Record<string, Object> = OAuthHttpClient.parseJsonObject(resultText);
162
+ let oauthError: string = OAuthHttpClient.asString(body, 'error');
163
+ if (oauthError.length > 0) {
164
+ errorCode = oauthError;
165
+ let desc: string = OAuthHttpClient.asString(body, 'error_description');
166
+ if (desc.length > 0) {
167
+ errorDescription = desc;
168
+ } else {
169
+ errorDescription = oauthError;
170
+ }
171
+ }
172
+ } catch (err) {
173
+ errorDescription = resultText.length > 200 ? resultText.substring(0, 200) : resultText;
174
+ }
175
+ }
176
+ return new OAuthError(errorCode, errorDescription);
177
+ }
178
+
179
+ /**
180
+ * Extract a human-readable message from a thrown value.
181
+ * HarmonyOS http/BusinessError objects stringify as "[object Object]" with `${err}`.
182
+ */
183
+ static errorMessage(err: Object): string {
184
+ if (err instanceof OAuthError) {
185
+ let full: string = err.message;
186
+ let sep: number = full.indexOf('::');
187
+ if (sep > 0) {
188
+ return full.substring(sep + 2);
189
+ }
190
+ return full;
191
+ }
192
+ if (typeof err === 'string') {
193
+ return err;
194
+ }
195
+ let nativeErr = err as NativeThrownError;
196
+ let nativeMessage: string = nativeErr.message;
197
+ if (typeof nativeMessage === 'string' && nativeMessage.length > 0 &&
198
+ nativeMessage !== '[object Object]') {
199
+ if (nativeErr.code !== undefined && nativeErr.code !== null) {
200
+ return `${nativeMessage} (code ${nativeErr.code})`;
201
+ }
202
+ return nativeMessage;
203
+ }
204
+ try {
205
+ let jsonText: string = JSON.stringify(err);
206
+ if (jsonText.length > 0 && jsonText !== '{}' && jsonText !== 'null') {
207
+ return jsonText;
208
+ }
209
+ } catch (ignored) {
210
+ }
211
+ let fallback: string = `${err}`;
212
+ if (fallback === '[object Object]') {
213
+ return 'Network request failed';
214
+ }
215
+ return fallback;
216
+ }
217
+
218
+ /**
219
+ * Normalize http result (string | Object | ArrayBuffer) to a string.
220
+ * ESObject is used because HttpResponse.result is a dynamic SDK type.
221
+ */
222
+ private static asResponseText(result: ESObject): string {
223
+ if (typeof result === 'string') {
224
+ return result;
225
+ }
226
+ if (result instanceof ArrayBuffer) {
227
+ try {
228
+ let decoder: util.TextDecoder = new util.TextDecoder();
229
+ return decoder.decodeToString(new Uint8Array(result));
230
+ } catch (err) {
231
+ hilog.error(DOMAIN, TAG, `TextDecoder decode failed: ${err}`);
232
+ return '';
233
+ }
234
+ }
235
+ // Object (rare) — best effort JSON serialize
236
+ try {
237
+ return JSON.stringify(result);
238
+ } catch (err) {
239
+ hilog.error(DOMAIN, TAG, `JSON.stringify result failed: ${err}`);
240
+ return '';
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Parse a JSON string into a string-keyed object map.
246
+ */
247
+ static parseJsonObject(text: string): Record<string, Object> {
248
+ if (text.length === 0) {
249
+ return {};
250
+ }
251
+ return JSON.parse(text) as Record<string, Object>;
252
+ }
253
+
254
+ /**
255
+ * Extract a string field from a parsed JSON map.
256
+ */
257
+ static asString(obj: Record<string, Object>, key: string): string {
258
+ let value: Object | undefined = obj[key];
259
+ if (value === undefined || value === null) {
260
+ return '';
261
+ }
262
+ return `${value}`;
263
+ }
264
+
265
+ /**
266
+ * Extract a number field from a parsed JSON map.
267
+ */
268
+ static asNumber(obj: Record<string, Object>, key: string): number {
269
+ let value: Object | undefined = obj[key];
270
+ if (value === undefined || value === null) {
271
+ return 0;
272
+ }
273
+ return Number.parseFloat(`${value}`);
274
+ }
275
+
276
+ /**
277
+ * Merge two header maps (first wins for duplicate keys).
278
+ */
279
+ static mergeHeaders(base: Record<string, string>, extra: Record<string, string>): Record<string, string> {
280
+ let result: Record<string, string> = {};
281
+ let baseKeys: string[] = Object.keys(base);
282
+ for (let key of baseKeys) {
283
+ result[key] = base[key];
284
+ }
285
+ let extraKeys: string[] = Object.keys(extra);
286
+ for (let key of extraKeys) {
287
+ if (result[key] === undefined) {
288
+ result[key] = extra[key];
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * OAuth / OIDC protocol helpers (RFC 6749 / 7636 / 7009, OIDC Core /
3
+ * Discovery / Registration / RP-Initiated Logout).
4
+ *
5
+ * Covers authorization request URL construction, redirect callback parsing,
6
+ * token/refresh/registration request bodies and the end_session URL.
7
+ */
8
+ import { AuthorizationResponse } from './Types';
9
+
10
+ export interface AuthorizationUrlParams {
11
+ authorizationEndpoint: string;
12
+ clientId: string;
13
+ redirectUrl: string;
14
+ scopes: string[];
15
+ state: string;
16
+ nonce: string;
17
+ codeChallenge: string;
18
+ additionalParameters: Record<string, string>;
19
+ }
20
+
21
+ export interface EndSessionUrlParams {
22
+ endSessionEndpoint: string;
23
+ idTokenHint: string;
24
+ postLogoutRedirectUri: string;
25
+ state: string;
26
+ additionalParameters: Record<string, string>;
27
+ }
28
+
29
+ export class OAuthProtocol {
30
+ /**
31
+ * Build the authorization request URL (response_type=code + PKCE/nonce +
32
+ * additional parameters). Every value is encodeURIComponent'd.
33
+ */
34
+ static buildAuthorizationUrl(params: AuthorizationUrlParams): string {
35
+ let queryParts: string[] = [];
36
+ queryParts.push(`client_id=${encodeURIComponent(params.clientId)}`);
37
+ queryParts.push(`redirect_uri=${encodeURIComponent(params.redirectUrl)}`);
38
+ queryParts.push('response_type=code');
39
+ if (params.scopes.length > 0) {
40
+ queryParts.push(`scope=${encodeURIComponent(params.scopes.join(' '))}`);
41
+ }
42
+ if (params.state.length > 0) {
43
+ queryParts.push(`state=${encodeURIComponent(params.state)}`);
44
+ }
45
+ if (params.nonce.length > 0) {
46
+ queryParts.push(`nonce=${encodeURIComponent(params.nonce)}`);
47
+ }
48
+ if (params.codeChallenge.length > 0) {
49
+ queryParts.push(`code_challenge=${encodeURIComponent(params.codeChallenge)}`);
50
+ queryParts.push('code_challenge_method=S256');
51
+ }
52
+ let keys: string[] = Object.keys(params.additionalParameters);
53
+ for (let key of keys) {
54
+ let value: string = params.additionalParameters[key];
55
+ queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
56
+ }
57
+ return `${params.authorizationEndpoint}?${queryParts.join('&')}`;
58
+ }
59
+
60
+ /**
61
+ * Build the RP-Initiated Logout URL (OIDC RP-Initiated Logout 1.0).
62
+ */
63
+ static buildEndSessionUrl(params: EndSessionUrlParams): string {
64
+ let queryParts: string[] = [];
65
+ if (params.idTokenHint.length > 0) {
66
+ queryParts.push(`id_token_hint=${encodeURIComponent(params.idTokenHint)}`);
67
+ }
68
+ if (params.postLogoutRedirectUri.length > 0) {
69
+ queryParts.push(`post_logout_redirect_uri=${encodeURIComponent(params.postLogoutRedirectUri)}`);
70
+ }
71
+ if (params.state.length > 0) {
72
+ queryParts.push(`state=${encodeURIComponent(params.state)}`);
73
+ }
74
+ let keys: string[] = Object.keys(params.additionalParameters);
75
+ for (let key of keys) {
76
+ let value: string = params.additionalParameters[key];
77
+ queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
78
+ }
79
+ if (queryParts.length === 0) {
80
+ return params.endSessionEndpoint;
81
+ }
82
+ return `${params.endSessionEndpoint}?${queryParts.join('&')}`;
83
+ }
84
+
85
+ /**
86
+ * Parse a redirect callback URL (query or fragment) into an
87
+ * AuthorizationResponse. state mismatches are surfaced by the caller via
88
+ * the returned state field.
89
+ */
90
+ static parseCallbackUrl(url: string): AuthorizationResponse {
91
+ let params: Record<string, string> = {};
92
+ OAuthProtocol.mergeUrlParams(params,
93
+ OAuthProtocol.parseQueryString(OAuthProtocol.extractQuery(url)));
94
+ OAuthProtocol.mergeUrlParams(params,
95
+ OAuthProtocol.parseQueryString(OAuthProtocol.extractFragment(url)));
96
+
97
+ let response: AuthorizationResponse = {
98
+ state: OAuthProtocol.getParam(params, 'state'),
99
+ authorizationCode: OAuthProtocol.getParam(params, 'code'),
100
+ error: OAuthProtocol.getParam(params, 'error'),
101
+ errorDescription: OAuthProtocol.getParam(params, 'error_description'),
102
+ scope: OAuthProtocol.getParam(params, 'scope'),
103
+ idToken: OAuthProtocol.getParam(params, 'id_token'),
104
+ accessToken: OAuthProtocol.getParam(params, 'access_token'),
105
+ tokenType: OAuthProtocol.getParam(params, 'token_type'),
106
+ accessTokenExpirationTime: 0,
107
+ additionalParameters: params,
108
+ };
109
+ let expiresIn: string = OAuthProtocol.getParam(params, 'expires_in');
110
+ if (expiresIn.length > 0) {
111
+ let seconds: number = Number.parseFloat(expiresIn);
112
+ if (!Number.isNaN(seconds)) {
113
+ response.accessTokenExpirationTime = Date.now() + seconds * 1000;
114
+ }
115
+ }
116
+ return response;
117
+ }
118
+
119
+ /**
120
+ * Build the token exchange request body (grant_type=authorization_code).
121
+ */
122
+ static buildTokenExchangeParams(code: string, redirectUrl: string, clientId: string,
123
+ codeVerifier: string, additionalParameters: Record<string, string>): Record<string, string> {
124
+ let params: Record<string, string> = {
125
+ 'grant_type': 'authorization_code',
126
+ 'code': code,
127
+ 'redirect_uri': redirectUrl,
128
+ 'client_id': clientId,
129
+ };
130
+ if (codeVerifier.length > 0) {
131
+ params.code_verifier = codeVerifier;
132
+ }
133
+ OAuthProtocol.mergeParams(params, additionalParameters);
134
+ return params;
135
+ }
136
+
137
+ /**
138
+ * Build the refresh token request body (grant_type=refresh_token).
139
+ */
140
+ static buildRefreshTokenParams(refreshToken: string, clientId: string,
141
+ scopes: string[], redirectUrl: string, additionalParameters: Record<string, string>): Record<string, string> {
142
+ let params: Record<string, string> = {
143
+ 'grant_type': 'refresh_token',
144
+ 'refresh_token': refreshToken,
145
+ 'client_id': clientId,
146
+ };
147
+ if (redirectUrl.length > 0) {
148
+ params.redirect_uri = redirectUrl;
149
+ }
150
+ if (scopes.length > 0) {
151
+ params.scope = scopes.join(' ');
152
+ }
153
+ OAuthProtocol.mergeParams(params, additionalParameters);
154
+ return params;
155
+ }
156
+
157
+ /**
158
+ * Build the dynamic client registration body (OIDC Registration 1.0).
159
+ */
160
+ static buildRegistrationBody(redirectUris: string[], responseTypes: string[],
161
+ grantTypes: string[], subjectType: string, tokenEndpointAuthMethod: string,
162
+ additionalParameters: Record<string, string>): Record<string, Object> {
163
+ let safeRedirectUris: string[] = OAuthProtocol.safeStringArray(redirectUris);
164
+ let safeResponseTypes: string[] = OAuthProtocol.safeStringArray(responseTypes);
165
+ let safeGrantTypes: string[] = OAuthProtocol.safeStringArray(grantTypes);
166
+ let safeSubjectType: string =
167
+ subjectType !== null && subjectType !== undefined ? subjectType : '';
168
+ let safeTokenEndpointAuthMethod: string =
169
+ tokenEndpointAuthMethod !== null && tokenEndpointAuthMethod !== undefined ?
170
+ tokenEndpointAuthMethod : '';
171
+ let body: Record<string, Object> = {};
172
+ body.redirect_uris = safeRedirectUris;
173
+ if (safeResponseTypes.length > 0) {
174
+ body.response_types = safeResponseTypes;
175
+ }
176
+ if (safeGrantTypes.length > 0) {
177
+ body.grant_types = safeGrantTypes;
178
+ }
179
+ if (safeSubjectType.length > 0) {
180
+ body.subject_type = safeSubjectType;
181
+ }
182
+ if (safeTokenEndpointAuthMethod.length > 0) {
183
+ body.token_endpoint_auth_method = safeTokenEndpointAuthMethod;
184
+ }
185
+ if (additionalParameters === null || additionalParameters === undefined) {
186
+ return body;
187
+ }
188
+ let keys: string[] = Object.keys(additionalParameters);
189
+ for (let key of keys) {
190
+ body[key] = additionalParameters[key];
191
+ }
192
+ return body;
193
+ }
194
+
195
+ /**
196
+ * Extract the query string from a URL (part after '?', before '#').
197
+ */
198
+ static extractQuery(url: string): string {
199
+ let queryStart: number = url.indexOf('?');
200
+ if (queryStart < 0) {
201
+ return '';
202
+ }
203
+ let fragmentStart: number = url.indexOf('#', queryStart);
204
+ if (fragmentStart < 0) {
205
+ return url.substring(queryStart + 1);
206
+ }
207
+ return url.substring(queryStart + 1, fragmentStart);
208
+ }
209
+
210
+ /**
211
+ * Extract the fragment string from a URL (part after '#').
212
+ */
213
+ static extractFragment(url: string): string {
214
+ let fragmentStart: number = url.indexOf('#');
215
+ if (fragmentStart < 0) {
216
+ return '';
217
+ }
218
+ return url.substring(fragmentStart + 1);
219
+ }
220
+
221
+ /**
222
+ * Parse a query/fragment string into a decoded parameter map.
223
+ */
224
+ static parseQueryString(query: string): Record<string, string> {
225
+ let params: Record<string, string> = {};
226
+ if (query.length === 0) {
227
+ return params;
228
+ }
229
+ let pairs: string[] = query.split('&');
230
+ for (let pair of pairs) {
231
+ if (pair.length === 0) {
232
+ continue;
233
+ }
234
+ let eqIndex: number = pair.indexOf('=');
235
+ if (eqIndex < 0) {
236
+ params[OAuthProtocol.safeDecode(pair)] = '';
237
+ } else {
238
+ let key: string = OAuthProtocol.safeDecode(pair.substring(0, eqIndex));
239
+ let value: string = OAuthProtocol.safeDecode(pair.substring(eqIndex + 1));
240
+ params[key] = value;
241
+ }
242
+ }
243
+ return params;
244
+ }
245
+
246
+ private static mergeUrlParams(target: Record<string, string>,
247
+ source: Record<string, string>): void {
248
+ let keys: string[] = Object.keys(source);
249
+ for (let key of keys) {
250
+ if (target[key] === undefined) {
251
+ target[key] = source[key];
252
+ }
253
+ }
254
+ }
255
+
256
+ private static mergeParams(target: Record<string, string>,
257
+ source: Record<string, string>): void {
258
+ let keys: string[] = Object.keys(source);
259
+ for (let key of keys) {
260
+ if (target[key] === undefined) {
261
+ target[key] = source[key];
262
+ }
263
+ }
264
+ }
265
+
266
+ private static getParam(params: Record<string, string>, key: string): string {
267
+ let value: string | undefined = params[key];
268
+ if (value === undefined) {
269
+ return '';
270
+ }
271
+ return value;
272
+ }
273
+
274
+ private static safeDecode(value: string): string {
275
+ try {
276
+ let plusRegex: RegExp = new RegExp('\\+', 'g');
277
+ return decodeURIComponent(value.replace(plusRegex, ' '));
278
+ } catch (err) {
279
+ return value;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * JS may pass null for omitted string[] args. Do not read `.length` on null.
285
+ */
286
+ private static safeStringArray(value: string[] | null | undefined): string[] {
287
+ if (value === null || value === undefined) {
288
+ let empty: string[] = [];
289
+ return empty;
290
+ }
291
+ return value;
292
+ }
293
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * PKCE (RFC 7636) helpers and secure random / state / nonce generation.
3
+ *
4
+ * - code_verifier: 32 random bytes Base64URL-encoded -> 43 chars (RFC 7636 §4.1).
5
+ * - code_challenge: SHA-256(code_verifier) Base64URL-encoded, method = S256.
6
+ * - state / nonce: 32 random bytes Base64URL-encoded.
7
+ *
8
+ * Uses @ohos.security.cryptoFramework (Random + SHA-256 Md) and
9
+ * @ohos.util Base64Helper (BASIC_URL_SAFE, no padding).
10
+ */
11
+ import { cryptoFramework } from '@kit.CryptoArchitectureKit';
12
+ import { util } from '@kit.ArkTS';
13
+ import hilog from '@ohos.hilog';
14
+ import { OAuthError } from './Types';
15
+
16
+ const DOMAIN: number = 0x0000;
17
+ const TAG: string = 'RNAppAuth';
18
+
19
+ export class PKCE {
20
+ /**
21
+ * Base64URL (RFC 4648 §5) encode a byte array, no '=' padding.
22
+ */
23
+ static base64UrlEncode(data: Uint8Array): string {
24
+ try {
25
+ let helper: util.Base64Helper = new util.Base64Helper();
26
+ return helper.encodeToStringSync(data, util.Type.BASIC_URL_SAFE);
27
+ } catch (err) {
28
+ hilog.error(DOMAIN, TAG, `base64UrlEncode failed: ${err}`);
29
+ return '';
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Generate a cryptographically random Base64URL string from `byteLength`
35
+ * random bytes (default 32 -> 43 chars output).
36
+ */
37
+ static generateRandomBase64Url(byteLength: number = 32): string {
38
+ try {
39
+ let random: cryptoFramework.Random = cryptoFramework.createRandom();
40
+ let blob: cryptoFramework.DataBlob = random.generateRandomSync(byteLength);
41
+ let bytes: Uint8Array = new Uint8Array(blob.data);
42
+ return PKCE.base64UrlEncode(bytes);
43
+ } catch (err) {
44
+ hilog.error(DOMAIN, TAG, `generateRandomBase64Url failed: ${err}`);
45
+ return '';
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Generate a random code_verifier (43 chars, RFC 7636 compliant).
51
+ */
52
+ static generateCodeVerifier(): string {
53
+ let verifier: string = PKCE.generateRandomBase64Url(32);
54
+ if (verifier.length === 0) {
55
+ // Last-resort fallback (not cryptographically strong): 43 chars from the
56
+ // unreserved alphabet, only used if the crypto provider is unavailable.
57
+ let alphabet: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
58
+ let fallback: string = '';
59
+ for (let i = 0; i < 43; i++) {
60
+ fallback += alphabet[i % alphabet.length];
61
+ }
62
+ return fallback;
63
+ }
64
+ return verifier;
65
+ }
66
+
67
+ /**
68
+ * Derive the S256 code_challenge from a code_verifier.
69
+ */
70
+ static async deriveCodeChallenge(codeVerifier: string): Promise<string> {
71
+ try {
72
+ let md: cryptoFramework.Md = cryptoFramework.createMd('SHA256');
73
+ let verifierBytes: Uint8Array = new util.TextEncoder().encodeInto(codeVerifier);
74
+ await md.update({ data: verifierBytes });
75
+ let digest: cryptoFramework.DataBlob = await md.digest();
76
+ let digestBytes: Uint8Array = new Uint8Array(digest.data);
77
+ return PKCE.base64UrlEncode(digestBytes);
78
+ } catch (err) {
79
+ hilog.error(DOMAIN, TAG, `deriveCodeChallenge failed: ${err}`);
80
+ throw new OAuthError('run_time_exception', `PKCE derivation failed: ${err}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Generate a random OAuth state value.
86
+ */
87
+ static generateState(): string {
88
+ let state: string = PKCE.generateRandomBase64Url(16);
89
+ if (state.length === 0) {
90
+ return `rnappauth_${Date.now()}`;
91
+ }
92
+ return state;
93
+ }
94
+
95
+ /**
96
+ * Generate a random OpenID Connect nonce value.
97
+ */
98
+ static generateNonce(): string {
99
+ let nonce: string = PKCE.generateRandomBase64Url(16);
100
+ if (nonce.length === 0) {
101
+ return `rnappauth_nonce_${Date.now()}`;
102
+ }
103
+ return nonce;
104
+ }
105
+ }