@symbiote-native/auth-session 0.0.0-stage → 0.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 (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -2
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +3 -0
  5. package/build/angular/use-auth-request.d.ts +7 -0
  6. package/build/angular/use-auth-request.js +11 -0
  7. package/build/core/auth-request-controllers.d.ts +44 -0
  8. package/build/core/auth-request-controllers.js +122 -0
  9. package/build/core/auth-request-hooks.d.ts +67 -0
  10. package/build/core/auth-request-hooks.js +75 -0
  11. package/build/core/auth-request.d.ts +29 -0
  12. package/build/core/auth-request.js +161 -0
  13. package/build/core/auth-request.types.d.ts +46 -0
  14. package/build/core/auth-request.types.js +23 -0
  15. package/build/core/auth-session.d.ts +10 -0
  16. package/build/core/auth-session.js +37 -0
  17. package/build/core/auth-session.types.d.ts +27 -0
  18. package/build/core/auth-session.types.js +1 -0
  19. package/build/core/base64.d.ts +1 -0
  20. package/build/core/base64.js +28 -0
  21. package/build/core/discovery.d.ts +57 -0
  22. package/build/core/discovery.js +29 -0
  23. package/build/core/errors.d.ts +29 -0
  24. package/build/core/errors.js +74 -0
  25. package/build/core/fetch.d.ts +15 -0
  26. package/build/core/fetch.js +40 -0
  27. package/build/core/index.d.ts +24 -0
  28. package/build/core/index.js +14 -0
  29. package/build/core/pkce.d.ts +9 -0
  30. package/build/core/pkce.js +41 -0
  31. package/build/core/prompt-string.d.ts +2 -0
  32. package/build/core/prompt-string.js +5 -0
  33. package/build/core/providers/facebook.d.ts +16 -0
  34. package/build/core/providers/facebook.js +43 -0
  35. package/build/core/providers/google-exchange-controller.d.ts +10 -0
  36. package/build/core/providers/google-exchange-controller.js +73 -0
  37. package/build/core/providers/google.d.ts +22 -0
  38. package/build/core/providers/google.js +52 -0
  39. package/build/core/providers/provider-hook-setup.d.ts +19 -0
  40. package/build/core/providers/provider-hook-setup.js +78 -0
  41. package/build/core/providers/provider-utils.d.ts +2 -0
  42. package/build/core/providers/provider-utils.js +8 -0
  43. package/build/core/providers/provider.types.d.ts +5 -0
  44. package/build/core/providers/provider.types.js +1 -0
  45. package/build/core/query-params.d.ts +5 -0
  46. package/build/core/query-params.js +12 -0
  47. package/build/core/session-result.d.ts +6 -0
  48. package/build/core/session-result.js +4 -0
  49. package/build/core/token-request.d.ts +89 -0
  50. package/build/core/token-request.js +328 -0
  51. package/build/core/token-request.types.d.ts +52 -0
  52. package/build/core/token-request.types.js +14 -0
  53. package/build/react/index.d.ts +3 -0
  54. package/build/react/index.js +2 -0
  55. package/build/react/use-auth-request.d.ts +20 -0
  56. package/build/react/use-auth-request.js +52 -0
  57. package/build/solid/index.d.ts +2 -0
  58. package/build/solid/index.js +2 -0
  59. package/build/solid/use-auth-request.d.ts +6 -0
  60. package/build/solid/use-auth-request.js +10 -0
  61. package/build/svelte/index.d.ts +2 -0
  62. package/build/svelte/index.js +2 -0
  63. package/build/svelte/use-auth-request.svelte.d.ts +28 -0
  64. package/build/svelte/use-auth-request.svelte.js +11 -0
  65. package/build/vue/index.d.ts +2 -0
  66. package/build/vue/index.js +2 -0
  67. package/build/vue/use-auth-request.d.ts +7 -0
  68. package/build/vue/use-auth-request.js +12 -0
  69. package/build-ngc/angular/index.d.ts +2 -0
  70. package/build-ngc/angular/index.js +4 -0
  71. package/build-ngc/angular/index.js.map +1 -0
  72. package/build-ngc/angular/use-auth-request.d.ts +7 -0
  73. package/build-ngc/angular/use-auth-request.js +12 -0
  74. package/build-ngc/angular/use-auth-request.js.map +1 -0
  75. package/build-ngc/core/auth-request-controllers.d.ts +44 -0
  76. package/build-ngc/core/auth-request-controllers.js +123 -0
  77. package/build-ngc/core/auth-request-controllers.js.map +1 -0
  78. package/build-ngc/core/auth-request-hooks.d.ts +67 -0
  79. package/build-ngc/core/auth-request-hooks.js +76 -0
  80. package/build-ngc/core/auth-request-hooks.js.map +1 -0
  81. package/build-ngc/core/auth-request.d.ts +29 -0
  82. package/build-ngc/core/auth-request.js +162 -0
  83. package/build-ngc/core/auth-request.js.map +1 -0
  84. package/build-ngc/core/auth-request.types.d.ts +46 -0
  85. package/build-ngc/core/auth-request.types.js +24 -0
  86. package/build-ngc/core/auth-request.types.js.map +1 -0
  87. package/build-ngc/core/auth-session.d.ts +10 -0
  88. package/build-ngc/core/auth-session.js +38 -0
  89. package/build-ngc/core/auth-session.js.map +1 -0
  90. package/build-ngc/core/auth-session.types.d.ts +27 -0
  91. package/build-ngc/core/auth-session.types.js +2 -0
  92. package/build-ngc/core/auth-session.types.js.map +1 -0
  93. package/build-ngc/core/base64.d.ts +1 -0
  94. package/build-ngc/core/base64.js +29 -0
  95. package/build-ngc/core/base64.js.map +1 -0
  96. package/build-ngc/core/discovery.d.ts +57 -0
  97. package/build-ngc/core/discovery.js +30 -0
  98. package/build-ngc/core/discovery.js.map +1 -0
  99. package/build-ngc/core/errors.d.ts +29 -0
  100. package/build-ngc/core/errors.js +75 -0
  101. package/build-ngc/core/errors.js.map +1 -0
  102. package/build-ngc/core/fetch.d.ts +15 -0
  103. package/build-ngc/core/fetch.js +41 -0
  104. package/build-ngc/core/fetch.js.map +1 -0
  105. package/build-ngc/core/index.d.ts +24 -0
  106. package/build-ngc/core/index.js +15 -0
  107. package/build-ngc/core/index.js.map +1 -0
  108. package/build-ngc/core/pkce.d.ts +9 -0
  109. package/build-ngc/core/pkce.js +42 -0
  110. package/build-ngc/core/pkce.js.map +1 -0
  111. package/build-ngc/core/prompt-string.d.ts +2 -0
  112. package/build-ngc/core/prompt-string.js +6 -0
  113. package/build-ngc/core/prompt-string.js.map +1 -0
  114. package/build-ngc/core/providers/facebook.d.ts +16 -0
  115. package/build-ngc/core/providers/facebook.js +44 -0
  116. package/build-ngc/core/providers/facebook.js.map +1 -0
  117. package/build-ngc/core/providers/google-exchange-controller.d.ts +10 -0
  118. package/build-ngc/core/providers/google-exchange-controller.js +74 -0
  119. package/build-ngc/core/providers/google-exchange-controller.js.map +1 -0
  120. package/build-ngc/core/providers/google.d.ts +22 -0
  121. package/build-ngc/core/providers/google.js +53 -0
  122. package/build-ngc/core/providers/google.js.map +1 -0
  123. package/build-ngc/core/providers/provider-hook-setup.d.ts +19 -0
  124. package/build-ngc/core/providers/provider-hook-setup.js +79 -0
  125. package/build-ngc/core/providers/provider-hook-setup.js.map +1 -0
  126. package/build-ngc/core/providers/provider-utils.d.ts +2 -0
  127. package/build-ngc/core/providers/provider-utils.js +9 -0
  128. package/build-ngc/core/providers/provider-utils.js.map +1 -0
  129. package/build-ngc/core/providers/provider.types.d.ts +5 -0
  130. package/build-ngc/core/providers/provider.types.js +2 -0
  131. package/build-ngc/core/providers/provider.types.js.map +1 -0
  132. package/build-ngc/core/query-params.d.ts +5 -0
  133. package/build-ngc/core/query-params.js +13 -0
  134. package/build-ngc/core/query-params.js.map +1 -0
  135. package/build-ngc/core/session-result.d.ts +6 -0
  136. package/build-ngc/core/session-result.js +5 -0
  137. package/build-ngc/core/session-result.js.map +1 -0
  138. package/build-ngc/core/token-request.d.ts +89 -0
  139. package/build-ngc/core/token-request.js +329 -0
  140. package/build-ngc/core/token-request.js.map +1 -0
  141. package/build-ngc/core/token-request.types.d.ts +52 -0
  142. package/build-ngc/core/token-request.types.js +15 -0
  143. package/build-ngc/core/token-request.types.js.map +1 -0
  144. package/package.json +151 -3
  145. package/src/angular/index.ts +11 -0
  146. package/src/angular/use-auth-request.ts +30 -0
  147. package/src/core/auth-request-controllers.ts +233 -0
  148. package/src/core/auth-request-hooks.ts +342 -0
  149. package/src/core/auth-request.ts +199 -0
  150. package/src/core/auth-request.types.ts +55 -0
  151. package/src/core/auth-session.ts +63 -0
  152. package/src/core/auth-session.types.ts +29 -0
  153. package/src/core/base64.ts +33 -0
  154. package/src/core/discovery.ts +102 -0
  155. package/src/core/errors.ts +102 -0
  156. package/src/core/fetch.ts +71 -0
  157. package/src/core/index.ts +97 -0
  158. package/src/core/pkce.ts +60 -0
  159. package/src/core/prompt-string.ts +8 -0
  160. package/src/core/providers/facebook.ts +66 -0
  161. package/src/core/providers/google-exchange-controller.ts +117 -0
  162. package/src/core/providers/google.ts +81 -0
  163. package/src/core/providers/provider-hook-setup.ts +117 -0
  164. package/src/core/providers/provider-utils.ts +18 -0
  165. package/src/core/providers/provider.types.ts +6 -0
  166. package/src/core/query-params.ts +20 -0
  167. package/src/core/session-result.ts +15 -0
  168. package/src/core/token-request.ts +452 -0
  169. package/src/core/token-request.types.ts +60 -0
  170. package/src/react/index.ts +11 -0
  171. package/src/react/use-auth-request.ts +151 -0
  172. package/src/solid/index.ts +10 -0
  173. package/src/solid/use-auth-request.ts +24 -0
  174. package/src/svelte/index.ts +10 -0
  175. package/src/svelte/svelte-ambient.d.ts +3 -0
  176. package/src/svelte/svelte-compile.test-helper.ts +7 -0
  177. package/src/svelte/use-auth-request.svelte.ts +22 -0
  178. package/src/vue/index.ts +10 -0
  179. package/src/vue/use-auth-request.ts +38 -0
@@ -0,0 +1,97 @@
1
+ export { AuthRequest } from './auth-request';
2
+ export {
3
+ CodeChallengeMethod,
4
+ Prompt,
5
+ ResponseType,
6
+ type IAuthDiscoveryDocument,
7
+ type IAuthRequestConfig,
8
+ type IAuthRequestPromptOptions,
9
+ type ICodeChallengeMethod,
10
+ } from './auth-request.types';
11
+ export { dismiss, loadAsync, makeRedirectUri } from './auth-session';
12
+ export type {
13
+ IAuthSessionRedirectUriOptions,
14
+ IAuthSessionResult,
15
+ } from './auth-session.types';
16
+ export type {
17
+ IDiscoveryDocument,
18
+ IIssuer,
19
+ IIssuerOrDiscovery,
20
+ IProviderMetadata,
21
+ } from './discovery';
22
+ export {
23
+ fetchDiscoveryAsync,
24
+ issuerWithWellKnownUrl,
25
+ resolveDiscoveryAsync,
26
+ } from './discovery';
27
+ export { AuthError, ResponseError, TokenError } from './errors';
28
+ export type { IAuthErrorConfig, IResponseErrorConfig } from './errors';
29
+ export { requestAsync } from './fetch';
30
+ export type { IFetchHeaders, IFetchRequest } from './fetch';
31
+ export {
32
+ AccessTokenRequest,
33
+ RefreshTokenRequest,
34
+ RevokeTokenRequest,
35
+ TokenRequest,
36
+ TokenResponse,
37
+ exchangeCodeAsync,
38
+ fetchUserInfoAsync,
39
+ getCurrentTimeInSeconds,
40
+ refreshAsync,
41
+ revokeAsync,
42
+ } from './token-request';
43
+ export {
44
+ GrantType,
45
+ TokenTypeHint,
46
+ type IAccessTokenRequestConfig,
47
+ type IRefreshTokenRequestConfig,
48
+ type IRevokeTokenRequestConfig,
49
+ type IServerTokenResponseConfig,
50
+ type ITokenRequestConfig,
51
+ type ITokenResponseConfig,
52
+ type ITokenType,
53
+ } from './token-request.types';
54
+ export {
55
+ AUTH_CELL_CHANGE,
56
+ createAutoDiscoveryController,
57
+ createLoadedRequestController,
58
+ createRequestResultController,
59
+ } from './auth-request-controllers';
60
+ export type {
61
+ IAuthCell,
62
+ IAuthRequestClass,
63
+ ILoadableRequest,
64
+ IPromptMethod,
65
+ IRequestResultHolder,
66
+ } from './auth-request-controllers';
67
+ export { GETTER_ARGS, createAuthRequestHooks } from './auth-request-hooks';
68
+ export type {
69
+ IApply,
70
+ IAuthHooksKit,
71
+ IAuthRequestHooks,
72
+ IAuthResourceController,
73
+ IBoxedKind,
74
+ IGetterKind,
75
+ IHkt,
76
+ } from './auth-request-hooks';
77
+ export { createGoogleExchangeController } from './providers/google-exchange-controller';
78
+ export {
79
+ resolveFacebookRequestSetup,
80
+ resolveGoogleRequestSetup,
81
+ toGoogleIdTokenConfig,
82
+ } from './providers/provider-hook-setup';
83
+ export type {
84
+ IGoogleExchangeParams,
85
+ IGoogleRequestSetup,
86
+ } from './providers/provider-hook-setup';
87
+ export type { IProviderAuthRequestConfig } from './providers/provider.types';
88
+ export {
89
+ FacebookAuthRequest,
90
+ discovery as facebookDiscovery,
91
+ } from './providers/facebook';
92
+ export type { IFacebookAuthRequestConfig } from './providers/facebook';
93
+ export {
94
+ GoogleAuthRequest,
95
+ discovery as googleDiscovery,
96
+ } from './providers/google';
97
+ export type { IGoogleAuthRequestConfig } from './providers/google';
@@ -0,0 +1,60 @@
1
+ import invariant from 'invariant';
2
+ import {
3
+ CryptoDigestAlgorithm,
4
+ CryptoEncoding,
5
+ digestStringAsync,
6
+ getRandomValues,
7
+ } from '@symbiote-native/crypto';
8
+
9
+ const CHARSET =
10
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
11
+
12
+ function convertBufferToString(buffer: Uint8Array): string {
13
+ const chars: string[] = [];
14
+ for (const byte of buffer) {
15
+ const char = CHARSET[byte % CHARSET.length];
16
+ if (char != null) {
17
+ chars.push(char);
18
+ }
19
+ }
20
+ return chars.join('');
21
+ }
22
+
23
+ function convertToUrlSafeString(b64: string): string {
24
+ return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
25
+ }
26
+
27
+ export function generateRandom(size: number): string {
28
+ const buffer = getRandomValues(new Uint8Array(size));
29
+ return convertBufferToString(buffer);
30
+ }
31
+
32
+ /** Proof Key for Code Exchange (RFC 7636), Section 4.1. */
33
+ export async function deriveChallengeAsync(code: string): Promise<string> {
34
+ invariant(
35
+ code.length > 42 && code.length < 129,
36
+ 'Invalid code length for PKCE.',
37
+ );
38
+
39
+ const buffer = await digestStringAsync(CryptoDigestAlgorithm.SHA256, code, {
40
+ encoding: CryptoEncoding.BASE64,
41
+ });
42
+ return convertToUrlSafeString(buffer);
43
+ }
44
+
45
+ export async function buildCodeAsync(
46
+ size: number = 128,
47
+ ): Promise<{ codeChallenge: string; codeVerifier: string }> {
48
+ const codeVerifier = generateRandom(size);
49
+ const codeChallenge = await deriveChallengeAsync(codeVerifier);
50
+ return { codeVerifier, codeChallenge };
51
+ }
52
+
53
+ /** Digests a random string with hex encoding, useful for creating `nonce`s. */
54
+ export async function generateHexStringAsync(size: number): Promise<string> {
55
+ const value = generateRandom(size);
56
+ const buffer = await digestStringAsync(CryptoDigestAlgorithm.SHA256, value, {
57
+ encoding: CryptoEncoding.HEX,
58
+ });
59
+ return convertToUrlSafeString(buffer);
60
+ }
@@ -0,0 +1,8 @@
1
+ import type { Prompt } from './auth-request.types';
2
+
3
+ export function createPromptString(
4
+ prompt: Prompt | Prompt[] | undefined,
5
+ ): string | undefined {
6
+ if (!prompt) return undefined;
7
+ return Array.isArray(prompt) ? prompt.join(' ') : prompt;
8
+ }
@@ -0,0 +1,66 @@
1
+ import type { IAuthRequestConfig } from '../auth-request.types';
2
+ import { ResponseType } from '../auth-request.types';
3
+ import { AuthRequest } from '../auth-request';
4
+ import type { IDiscoveryDocument } from '../discovery';
5
+ import { generateHexStringAsync } from '../pkce';
6
+ import { applyRequiredScopes } from './provider-utils';
7
+ import type { IProviderAuthRequestConfig } from './provider.types';
8
+
9
+ /** Required for Firebase to work properly - a reasonable default either way. */
10
+ const MINIMUM_SCOPES = ['public_profile', 'email'];
11
+
12
+ export const discovery: IDiscoveryDocument = {
13
+ authorizationEndpoint: 'https://www.facebook.com/v6.0/dialog/oauth',
14
+ tokenEndpoint: 'https://graph.facebook.com/v6.0/oauth/access_token',
15
+ };
16
+
17
+ export type IFacebookAuthRequestConfig = IProviderAuthRequestConfig & {
18
+ webClientId?: string;
19
+ iosClientId?: string;
20
+ androidClientId?: string;
21
+ };
22
+
23
+ /** Extends `AuthRequest`, applying Facebook's minimum scopes and an auth nonce. */
24
+ export class FacebookAuthRequest extends AuthRequest {
25
+ nonce?: string;
26
+
27
+ constructor({
28
+ language,
29
+ extraParams = {},
30
+ clientSecret,
31
+ ...config
32
+ }: IFacebookAuthRequestConfig) {
33
+ const inputParams: Record<string, string> = {
34
+ display: 'popup',
35
+ ...extraParams,
36
+ };
37
+ if (language) {
38
+ inputParams.locale = language;
39
+ }
40
+
41
+ const scopes = applyRequiredScopes(config.scopes, MINIMUM_SCOPES);
42
+ // Facebook rejects a client secret unless the code flow is used.
43
+ const inputClientSecret =
44
+ config.responseType && config.responseType !== ResponseType.Code
45
+ ? clientSecret
46
+ : undefined;
47
+
48
+ super({
49
+ ...config,
50
+ responseType: config.responseType ?? ResponseType.Token,
51
+ clientSecret: inputClientSecret,
52
+ scopes,
53
+ extraParams: inputParams,
54
+ });
55
+ }
56
+
57
+ override async getAuthRequestConfigAsync(): Promise<IAuthRequestConfig> {
58
+ const { extraParams = {}, ...config } =
59
+ await super.getAuthRequestConfigAsync();
60
+ if (!extraParams.nonce) {
61
+ this.nonce = this.nonce ?? (await generateHexStringAsync(16));
62
+ extraParams.auth_nonce = this.nonce;
63
+ }
64
+ return { ...config, extraParams };
65
+ }
66
+ }
@@ -0,0 +1,117 @@
1
+ // The automatic code exchange of upstream's Google `useAuthRequest`, as a cell that follows the
2
+ // prompt result and swaps in the exchanged authentication once it arrives
3
+
4
+ import { createResourceController } from '@symbiote-native/engine';
5
+ import type { IAuthCell, ILoadableRequest } from '../auth-request-controllers';
6
+ import { createAuthCell } from '../auth-request-controllers';
7
+ import type { IAuthSessionResult } from '../auth-session.types';
8
+ import type { IAuthSessionParamsResult } from '../session-result';
9
+ import { isSuccessResult } from '../session-result';
10
+ import { AccessTokenRequest } from '../token-request';
11
+ import { discovery as googleDiscovery } from './google';
12
+ import type { IGoogleExchangeParams } from './provider-hook-setup';
13
+
14
+ type ICodeSource = Pick<ILoadableRequest, 'codeVerifier'>;
15
+ type IExchangeKey = {
16
+ result: IAuthSessionResult | null;
17
+ request: ICodeSource | null;
18
+ exchange: IGoogleExchangeParams;
19
+ };
20
+ type IResultCell = ReturnType<typeof createAuthCell<IAuthSessionResult | null>>;
21
+ type IExchangeResource = { cell: IResultCell; stop: () => void };
22
+
23
+ // The result to exchange, or null when there is nothing to do: a code and no authentication yet,
24
+ // unless the caller decided explicitly
25
+ function exchangeableResult(
26
+ result: IAuthSessionResult | null,
27
+ override: boolean | undefined,
28
+ ): IAuthSessionParamsResult | null {
29
+ if (!isSuccessResult(result)) return null;
30
+ const shouldExchange =
31
+ override ?? (Boolean(result.params.code) && !result.authentication);
32
+ return shouldExchange ? result : null;
33
+ }
34
+
35
+ // Trades the code for tokens and folds them into the result, as the hook does upstream
36
+ async function exchangeCode(
37
+ toExchange: IAuthSessionParamsResult,
38
+ request: ICodeSource | null,
39
+ exchange: IGoogleExchangeParams,
40
+ ): Promise<IAuthSessionResult> {
41
+ const exchangeRequest = new AccessTokenRequest({
42
+ clientId: exchange.clientId,
43
+ clientSecret: exchange.clientSecret,
44
+ redirectUri: exchange.redirectUri,
45
+ scopes: exchange.scopes,
46
+ code: toExchange.params.code ?? '',
47
+ extraParams: { code_verifier: request?.codeVerifier || '' },
48
+ });
49
+ const authentication = await exchangeRequest.performAsync(googleDiscovery);
50
+ return {
51
+ ...toExchange,
52
+ params: {
53
+ id_token: authentication?.idToken || '',
54
+ access_token: authentication.accessToken,
55
+ ...toExchange.params,
56
+ },
57
+ authentication,
58
+ };
59
+ }
60
+
61
+ function isSameKey(a: IExchangeKey, b: IExchangeKey): boolean {
62
+ return (
63
+ a.result === b.result &&
64
+ a.request?.codeVerifier === b.request?.codeVerifier &&
65
+ a.exchange.clientId === b.exchange.clientId &&
66
+ a.exchange.redirectUri === b.exchange.redirectUri &&
67
+ a.exchange.clientSecret === b.exchange.clientSecret &&
68
+ a.exchange.shouldAutoExchangeCode === b.exchange.shouldAutoExchangeCode &&
69
+ a.exchange.scopes?.join(',') === b.exchange.scopes?.join(',')
70
+ );
71
+ }
72
+
73
+ export function createGoogleExchangeController(): {
74
+ resolve: (
75
+ result: IAuthSessionResult | null,
76
+ request: ICodeSource | null,
77
+ exchange: IGoogleExchangeParams,
78
+ ) => IAuthCell<IAuthSessionResult | null>;
79
+ flushDispose: () => void;
80
+ dispose: () => void;
81
+ } {
82
+ // The last published value stays visible while the next exchange runs
83
+ let lastPublished: IAuthSessionResult | null = null;
84
+
85
+ const controller = createResourceController<IExchangeKey, IExchangeResource>(
86
+ ({ result, request, exchange }) => {
87
+ let isMounted = true;
88
+ const stop = (): void => {
89
+ isMounted = false;
90
+ };
91
+ const toExchange = exchangeableResult(
92
+ result,
93
+ exchange.shouldAutoExchangeCode,
94
+ );
95
+ if (!toExchange) {
96
+ lastPublished = result;
97
+ return { cell: createAuthCell(result), stop };
98
+ }
99
+ const cell = createAuthCell<IAuthSessionResult | null>(lastPublished);
100
+ void exchangeCode(toExchange, request, exchange).then(merged => {
101
+ if (!isMounted) return;
102
+ lastPublished = merged;
103
+ cell.set(merged);
104
+ });
105
+ return { cell, stop };
106
+ },
107
+ resource => resource.stop(),
108
+ isSameKey,
109
+ );
110
+
111
+ return {
112
+ resolve: (result, request, exchange) =>
113
+ controller.resolve({ result, request, exchange }).cell,
114
+ flushDispose: controller.flushDispose,
115
+ dispose: controller.dispose,
116
+ };
117
+ }
@@ -0,0 +1,81 @@
1
+ import type { IAuthRequestConfig } from '../auth-request.types';
2
+ import { Prompt, ResponseType } from '../auth-request.types';
3
+ import { AuthRequest } from '../auth-request';
4
+ import type { IDiscoveryDocument } from '../discovery';
5
+ import { generateHexStringAsync } from '../pkce';
6
+ import { applyRequiredScopes } from './provider-utils';
7
+ import type { IProviderAuthRequestConfig } from './provider.types';
8
+
9
+ const MINIMUM_SCOPES = [
10
+ 'openid',
11
+ 'https://www.googleapis.com/auth/userinfo.profile',
12
+ 'https://www.googleapis.com/auth/userinfo.email',
13
+ ];
14
+
15
+ export const discovery: IDiscoveryDocument = {
16
+ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
17
+ tokenEndpoint: 'https://oauth2.googleapis.com/token',
18
+ revocationEndpoint: 'https://oauth2.googleapis.com/revoke',
19
+ userInfoEndpoint: 'https://openidconnect.googleapis.com/v1/userinfo',
20
+ };
21
+
22
+ export type IGoogleAuthRequestConfig = IProviderAuthRequestConfig & {
23
+ /** If the user's email is known ahead of time, supply it as the default option. */
24
+ loginHint?: string;
25
+ /** Lets the user switch accounts, where possible. @default false */
26
+ selectAccount?: boolean;
27
+ webClientId?: string;
28
+ iosClientId?: string;
29
+ androidClientId?: string;
30
+ /** Exchange the response code for a token in the hook, on by default for the code flow */
31
+ shouldAutoExchangeCode?: boolean;
32
+ };
33
+
34
+ /** Extends `AuthRequest`, applying Google's minimum scopes and an id-token nonce. */
35
+ export class GoogleAuthRequest extends AuthRequest {
36
+ nonce?: string;
37
+
38
+ constructor({
39
+ language,
40
+ loginHint,
41
+ selectAccount,
42
+ extraParams = {},
43
+ clientSecret,
44
+ ...config
45
+ }: IGoogleAuthRequestConfig) {
46
+ const inputParams = { ...extraParams };
47
+ if (language) inputParams.hl = language;
48
+ if (loginHint) inputParams.login_hint = loginHint;
49
+ if (selectAccount) inputParams.prompt = Prompt.SelectAccount;
50
+
51
+ const scopes = applyRequiredScopes(config.scopes, MINIMUM_SCOPES);
52
+ const isImplicit =
53
+ config.responseType === ResponseType.Token ||
54
+ config.responseType === ResponseType.IdToken;
55
+ if (isImplicit) {
56
+ config.usePKCE = false;
57
+ }
58
+ // Google rejects a client secret unless the code flow is used.
59
+ const inputClientSecret =
60
+ config.responseType && config.responseType !== ResponseType.Code
61
+ ? clientSecret
62
+ : undefined;
63
+
64
+ super({
65
+ ...config,
66
+ clientSecret: inputClientSecret,
67
+ scopes,
68
+ extraParams: inputParams,
69
+ });
70
+ }
71
+
72
+ override async getAuthRequestConfigAsync(): Promise<IAuthRequestConfig> {
73
+ const { extraParams = {}, ...config } =
74
+ await super.getAuthRequestConfigAsync();
75
+ if (config.responseType === ResponseType.IdToken && !extraParams.nonce) {
76
+ this.nonce = this.nonce ?? (await generateHexStringAsync(16));
77
+ extraParams.nonce = this.nonce;
78
+ }
79
+ return { ...config, extraParams };
80
+ }
81
+ }
@@ -0,0 +1,117 @@
1
+ // The config each provider hook derives before it loads a request, kept pure so every adapter's
2
+ // hook shares one copy of upstream's `useMemo` blocks
3
+
4
+ import { Platform } from 'react-native';
5
+ import { applicationId } from '@symbiote-native/application';
6
+ import { Prompt, ResponseType } from '../auth-request.types';
7
+ import { makeRedirectUri } from '../auth-session';
8
+ import type { IAuthSessionRedirectUriOptions } from '../auth-session.types';
9
+ import type { IFacebookAuthRequestConfig } from './facebook';
10
+ import type { IGoogleAuthRequestConfig } from './google';
11
+ import { invariantClientId } from './provider-utils';
12
+
13
+ type IClientIdProperty = 'iosClientId' | 'androidClientId' | 'webClientId';
14
+
15
+ function clientIdProperty(): IClientIdProperty {
16
+ return Platform.select<IClientIdProperty>({
17
+ ios: 'iosClientId',
18
+ android: 'androidClientId',
19
+ default: 'webClientId',
20
+ });
21
+ }
22
+
23
+ function pickClientId(
24
+ config: Partial<IGoogleAuthRequestConfig | IFacebookAuthRequestConfig>,
25
+ providerName: string,
26
+ ): string {
27
+ const propertyName = clientIdProperty();
28
+ const clientId = config[propertyName] ?? config.clientId;
29
+ invariantClientId(propertyName, clientId, providerName);
30
+ return clientId;
31
+ }
32
+
33
+ /** What the automatic code exchange needs, upstream reads the same fields off the hook config */
34
+ export type IGoogleExchangeParams = {
35
+ clientId: string;
36
+ clientSecret?: string;
37
+ redirectUri: string;
38
+ scopes?: string[];
39
+ shouldAutoExchangeCode?: boolean;
40
+ };
41
+
42
+ export type IGoogleRequestSetup = {
43
+ requestConfig: IGoogleAuthRequestConfig;
44
+ exchange: IGoogleExchangeParams;
45
+ };
46
+
47
+ function resolveGoogleResponseType(
48
+ config: Partial<IGoogleAuthRequestConfig>,
49
+ ): IGoogleAuthRequestConfig['responseType'] {
50
+ // Allow overrides
51
+ if (typeof config.responseType !== 'undefined') return config.responseType;
52
+ // Installed apps auto exchange the code for the token and the id token, no secret needed
53
+ return ResponseType.Code;
54
+ }
55
+
56
+ function resolveGoogleExtraParams(
57
+ config: Partial<IGoogleAuthRequestConfig>,
58
+ ): Record<string, string> {
59
+ const output: Record<string, string> = { ...config.extraParams };
60
+ if (config.language) output.hl = config.language;
61
+ if (config.loginHint) output.login_hint = config.loginHint;
62
+ if (config.selectAccount) output.prompt = Prompt.SelectAccount;
63
+ return output;
64
+ }
65
+
66
+ export function resolveGoogleRequestSetup(
67
+ config: Partial<IGoogleAuthRequestConfig> = {},
68
+ redirectUriOptions: Partial<IAuthSessionRedirectUriOptions> = {},
69
+ ): IGoogleRequestSetup {
70
+ const clientId = pickClientId(config, 'Google');
71
+ const redirectUri =
72
+ config.redirectUri ??
73
+ makeRedirectUri({
74
+ native: `${applicationId}:/oauthredirect`,
75
+ ...redirectUriOptions,
76
+ });
77
+ return {
78
+ requestConfig: {
79
+ ...config,
80
+ responseType: resolveGoogleResponseType(config),
81
+ extraParams: resolveGoogleExtraParams(config),
82
+ clientId,
83
+ redirectUri,
84
+ },
85
+ exchange: {
86
+ clientId,
87
+ clientSecret: config.clientSecret,
88
+ redirectUri,
89
+ scopes: config.scopes,
90
+ shouldAutoExchangeCode: config.shouldAutoExchangeCode,
91
+ },
92
+ };
93
+ }
94
+
95
+ /** Upstream only asks for `id_token` directly on web, so off web the default flow applies */
96
+ export function toGoogleIdTokenConfig(
97
+ config: Partial<IGoogleAuthRequestConfig>,
98
+ ): Partial<IGoogleAuthRequestConfig> {
99
+ return { ...config, responseType: undefined };
100
+ }
101
+
102
+ export function resolveFacebookRequestSetup(
103
+ config: Partial<IFacebookAuthRequestConfig> = {},
104
+ redirectUriOptions: Partial<IAuthSessionRedirectUriOptions> = {},
105
+ ): IFacebookAuthRequestConfig {
106
+ const clientId = pickClientId(config, 'Facebook');
107
+ const redirectUri =
108
+ config.redirectUri ??
109
+ makeRedirectUri({
110
+ // The redirect URI is fb + client ID on native
111
+ native: `fb${clientId}://authorize`,
112
+ ...redirectUriOptions,
113
+ });
114
+ const extraParams: Record<string, string> = { ...config.extraParams };
115
+ if (config.language) extraParams.locale = config.language;
116
+ return { ...config, extraParams, clientId, redirectUri };
117
+ }
@@ -0,0 +1,18 @@
1
+ export function applyRequiredScopes(
2
+ scopes: string[] = [],
3
+ requiredScopes: string[],
4
+ ): string[] {
5
+ return [...new Set([...scopes, ...requiredScopes])];
6
+ }
7
+
8
+ export function invariantClientId(
9
+ idName: string,
10
+ value: string | undefined,
11
+ providerName: string,
12
+ ): asserts value is string {
13
+ if (typeof value === 'undefined') {
14
+ throw new Error(
15
+ `Client Id property \`${idName}\` must be defined to use ${providerName} auth on this platform.`,
16
+ );
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ import type { IAuthRequestConfig } from '../auth-request.types';
2
+
3
+ export type IProviderAuthRequestConfig = IAuthRequestConfig & {
4
+ /** ISO 639-1 code, optionally `-<ISO 3166-1 alpha-2>` (e.g. `it`, `pt-PT`). */
5
+ language?: string;
6
+ };
@@ -0,0 +1,20 @@
1
+ export type IQueryParamsResult = {
2
+ errorCode: string | null;
3
+ params: Record<string, string>;
4
+ };
5
+
6
+ export function getQueryParams(input: string): IQueryParamsResult {
7
+ const url = new URL(input, 'https://phony.example');
8
+
9
+ const errorCode = url.searchParams.get('errorCode');
10
+ url.searchParams.delete('errorCode');
11
+
12
+ const params = Object.fromEntries(url.searchParams);
13
+ if (url.hash) {
14
+ new URLSearchParams(url.hash.replace(/^#/, '')).forEach((value, key) => {
15
+ params[key] = value;
16
+ });
17
+ }
18
+
19
+ return { errorCode, params };
20
+ }
@@ -0,0 +1,15 @@
1
+ import type { IAuthSessionResult } from './auth-session.types';
2
+
3
+ const SUCCESS_TYPE = 'success' satisfies IAuthSessionResult['type'];
4
+
5
+ /** The variant that carries params and authentication */
6
+ export type IAuthSessionParamsResult = Extract<
7
+ IAuthSessionResult,
8
+ { params: unknown }
9
+ >;
10
+
11
+ export function isSuccessResult(
12
+ result: IAuthSessionResult | null,
13
+ ): result is IAuthSessionParamsResult {
14
+ return result?.type === SUCCESS_TYPE;
15
+ }