@symbiote-native/auth-session 0.0.0-stage → 0.1.1

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,199 @@
1
+ import { Platform } from 'react-native';
2
+ import invariant from 'invariant';
3
+ import { openAuthSessionAsync } from '@symbiote-native/web-browser';
4
+ import type { IWebBrowserAuthSessionResult } from '@symbiote-native/web-browser';
5
+ import type {
6
+ IAuthDiscoveryDocument,
7
+ IAuthRequestConfig,
8
+ IAuthRequestPromptOptions,
9
+ Prompt,
10
+ } from './auth-request.types';
11
+ import { CodeChallengeMethod, ResponseType } from './auth-request.types';
12
+ import type { IAuthSessionResult } from './auth-session.types';
13
+ import { AuthError } from './errors';
14
+ import { createPromptString } from './prompt-string';
15
+ import { getQueryParams } from './query-params';
16
+ import { buildCodeAsync, generateRandom } from './pkce';
17
+ import { TokenResponse } from './token-request';
18
+
19
+ let authLock = false;
20
+
21
+ /** Manages an authorization request, https://tools.ietf.org/html/rfc6749#section-4.1.1 */
22
+ export class AuthRequest implements Omit<IAuthRequestConfig, 'state'> {
23
+ /** Protection against Cross-Site Request Forgery. */
24
+ public state: string;
25
+ public url: string | null = null;
26
+ public codeVerifier?: string;
27
+ public codeChallenge?: string;
28
+
29
+ readonly responseType: ResponseType | string;
30
+ readonly clientId: string;
31
+ readonly extraParams: Record<string, string>;
32
+ readonly usePKCE?: boolean;
33
+ readonly codeChallengeMethod: CodeChallengeMethod;
34
+ readonly redirectUri: string;
35
+ readonly scopes?: string[];
36
+ readonly clientSecret?: string;
37
+ readonly prompt?: Prompt | Prompt[];
38
+
39
+ constructor(request: IAuthRequestConfig) {
40
+ this.responseType = request.responseType ?? ResponseType.Code;
41
+ this.clientId = request.clientId;
42
+ this.redirectUri = request.redirectUri;
43
+ this.scopes = request.scopes;
44
+ this.clientSecret = request.clientSecret;
45
+ this.prompt = request.prompt;
46
+ this.state = request.state ?? generateRandom(10);
47
+ this.extraParams = request.extraParams ?? {};
48
+ this.codeChallengeMethod =
49
+ request.codeChallengeMethod ?? CodeChallengeMethod.S256;
50
+ this.usePKCE = request.usePKCE ?? true;
51
+
52
+ invariant(
53
+ this.codeChallengeMethod !== CodeChallengeMethod.Plain,
54
+ '`AuthRequest` does not support `CodeChallengeMethod.Plain` as it is not secure.',
55
+ );
56
+ invariant(
57
+ this.redirectUri,
58
+ `\`AuthRequest\` requires a valid \`redirectUri\`. Ex: ${
59
+ Platform.OS === 'web'
60
+ ? 'https://yourwebsite.com/'
61
+ : 'com.your.app:/oauthredirect'
62
+ }`,
63
+ );
64
+ }
65
+
66
+ /** Loads and returns a valid auth request config, generating PKCE fields if needed. */
67
+ async getAuthRequestConfigAsync(): Promise<IAuthRequestConfig> {
68
+ if (this.usePKCE) {
69
+ await this.ensureCodeIsSetupAsync();
70
+ }
71
+
72
+ return {
73
+ responseType: this.responseType,
74
+ clientId: this.clientId,
75
+ redirectUri: this.redirectUri,
76
+ scopes: this.scopes,
77
+ clientSecret: this.clientSecret,
78
+ codeChallenge: this.codeChallenge,
79
+ codeChallengeMethod: this.codeChallengeMethod,
80
+ prompt: this.prompt,
81
+ state: this.state,
82
+ extraParams: this.extraParams,
83
+ usePKCE: this.usePKCE,
84
+ };
85
+ }
86
+
87
+ /** Prompts the user to authorize, opening a browser session for the auth URL. */
88
+ async promptAsync(
89
+ discovery: IAuthDiscoveryDocument,
90
+ { url, ...options }: IAuthRequestPromptOptions = {},
91
+ ): Promise<IAuthSessionResult> {
92
+ if (!url) {
93
+ if (!this.url) {
94
+ return this.promptAsync(discovery, {
95
+ ...options,
96
+ url: await this.makeAuthUrlAsync(discovery),
97
+ });
98
+ }
99
+ url = this.url;
100
+ }
101
+ invariant(
102
+ url,
103
+ 'No authUrl provided - it points to the page where the user signs in.',
104
+ );
105
+
106
+ if (authLock) {
107
+ return { type: 'locked' };
108
+ }
109
+ authLock = true;
110
+
111
+ let result: IWebBrowserAuthSessionResult;
112
+ try {
113
+ result = await openAuthSessionAsync(url, this.redirectUri, options);
114
+ } finally {
115
+ authLock = false;
116
+ }
117
+
118
+ if (result.type === 'opened') {
119
+ throw new Error('An unexpected error occurred');
120
+ }
121
+ if (result.type !== 'success') {
122
+ return { type: result.type };
123
+ }
124
+ return this.parseReturnUrl(result.url);
125
+ }
126
+
127
+ parseReturnUrl(url: string): IAuthSessionResult {
128
+ const { params, errorCode } = getQueryParams(url);
129
+ const { state, error = errorCode ?? undefined } = params;
130
+
131
+ let parsedError: AuthError | null = null;
132
+ let authentication: TokenResponse | null = null;
133
+ if (state !== this.state) {
134
+ parsedError = new AuthError({
135
+ error: 'state_mismatch',
136
+ error_description:
137
+ 'Cross-Site request verification failed. Cached state and returned state do not match.',
138
+ });
139
+ } else if (error) {
140
+ parsedError = new AuthError({ error, ...params });
141
+ }
142
+ if (params.access_token) {
143
+ authentication = TokenResponse.fromQueryParams(params);
144
+ }
145
+
146
+ return {
147
+ type: parsedError ? 'error' : 'success',
148
+ error: parsedError,
149
+ url,
150
+ params,
151
+ authentication,
152
+ errorCode,
153
+ };
154
+ }
155
+
156
+ /** Builds and caches the authorization URL. */
157
+ async makeAuthUrlAsync(discovery: IAuthDiscoveryDocument): Promise<string> {
158
+ const request = await this.getAuthRequestConfigAsync();
159
+ if (!request.state)
160
+ throw new Error('Cannot make request URL without a valid `state` loaded');
161
+
162
+ const params: Record<string, string> = {};
163
+ if (request.codeChallenge) {
164
+ params.code_challenge = request.codeChallenge;
165
+ }
166
+ for (const extra in request.extraParams) {
167
+ const param = request.extraParams[extra];
168
+ if (param != null) {
169
+ params[extra] = param;
170
+ }
171
+ }
172
+ if (request.usePKCE && request.codeChallengeMethod) {
173
+ params.code_challenge_method = request.codeChallengeMethod;
174
+ }
175
+ if (request.clientSecret) {
176
+ params.client_secret = request.clientSecret;
177
+ }
178
+ if (request.prompt) {
179
+ params.prompt = createPromptString(request.prompt) ?? '';
180
+ }
181
+ params.redirect_uri = request.redirectUri;
182
+ params.client_id = request.clientId;
183
+ params.response_type = request.responseType ?? ResponseType.Code;
184
+ params.state = request.state;
185
+ if (request.scopes?.length) {
186
+ params.scope = request.scopes.join(' ');
187
+ }
188
+
189
+ this.url = `${discovery.authorizationEndpoint}?${new URLSearchParams(params)}`;
190
+ return this.url;
191
+ }
192
+
193
+ private async ensureCodeIsSetupAsync(): Promise<void> {
194
+ if (this.codeVerifier) return;
195
+ const { codeVerifier, codeChallenge } = await buildCodeAsync();
196
+ this.codeVerifier = codeVerifier;
197
+ this.codeChallenge = codeChallenge;
198
+ }
199
+ }
@@ -0,0 +1,55 @@
1
+ import type { IAuthSessionOpenOptions } from '@symbiote-native/web-browser';
2
+ import type { IDiscoveryDocument } from './discovery';
3
+
4
+ export enum CodeChallengeMethod {
5
+ /** The default and recommended method for transforming the code verifier. */
6
+ S256 = 'S256',
7
+ /** Not secure - the code verifier would be sent to the server as-is. */
8
+ Plain = 'plain',
9
+ }
10
+ export type ICodeChallengeMethod = CodeChallengeMethod;
11
+
12
+ /** https://tools.ietf.org/html/rfc6749#section-3.1.1 */
13
+ export enum ResponseType {
14
+ Code = 'code',
15
+ Token = 'token',
16
+ /** A custom registered type for getting an `id_token` from Google OAuth. */
17
+ IdToken = 'id_token',
18
+ }
19
+
20
+ /** https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationRequest */
21
+ export enum Prompt {
22
+ None = 'none',
23
+ Login = 'login',
24
+ Consent = 'consent',
25
+ SelectAccount = 'select_account',
26
+ }
27
+
28
+ export type IAuthRequestPromptOptions = IAuthSessionOpenOptions & {
29
+ /** URL to open when prompting the user; usually left undefined and generated internally. */
30
+ url?: string;
31
+ };
32
+
33
+ /** Represents an OAuth authorization request as JSON. */
34
+ export type IAuthRequestConfig = {
35
+ /** @default ResponseType.Code */
36
+ responseType?: ResponseType | string;
37
+ clientId: string;
38
+ redirectUri: string;
39
+ scopes?: string[];
40
+ clientSecret?: string;
41
+ /** @default CodeChallengeMethod.S256 */
42
+ codeChallengeMethod?: CodeChallengeMethod;
43
+ codeChallenge?: string;
44
+ prompt?: Prompt | Prompt[];
45
+ /** Protection against Cross-Site Request Forgery. */
46
+ state?: string;
47
+ extraParams?: Record<string, string>;
48
+ /** @default true */
49
+ usePKCE?: boolean;
50
+ };
51
+
52
+ export type IAuthDiscoveryDocument = Pick<
53
+ IDiscoveryDocument,
54
+ 'authorizationEndpoint'
55
+ >;
@@ -0,0 +1,63 @@
1
+ import { Platform } from 'react-native';
2
+ import { dismissAuthSession } from '@symbiote-native/web-browser';
3
+ import { AuthRequest } from './auth-request';
4
+ import type { IAuthRequestConfig } from './auth-request.types';
5
+ import type { IAuthSessionRedirectUriOptions } from './auth-session.types';
6
+ import type { IIssuerOrDiscovery } from './discovery';
7
+ import { resolveDiscoveryAsync } from './discovery';
8
+
9
+ const IPV4_PATTERN =
10
+ /\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/;
11
+
12
+ /** Cancels an active `AuthSession`, if there is one. */
13
+ export function dismiss(): void {
14
+ dismissAuthSession();
15
+ }
16
+
17
+ /** Unlike upstream, never reads an app manifest - pass `native` or `scheme` explicitly. */
18
+ export function makeRedirectUri({
19
+ native,
20
+ scheme,
21
+ isTripleSlashed = false,
22
+ queryParams = {},
23
+ path = '',
24
+ preferLocalhost = false,
25
+ }: IAuthSessionRedirectUriOptions = {}): string {
26
+ if (Platform.OS !== 'web' && native) {
27
+ return native;
28
+ }
29
+ if (!scheme) {
30
+ throw new Error(
31
+ 'makeRedirectUri() requires a `scheme` (or a `native` URI on non-web platforms) - there is no app manifest to infer one from.',
32
+ );
33
+ }
34
+
35
+ const cleanedPath = path.replace(/^\//, '');
36
+ const query = new URLSearchParams(
37
+ Object.entries(queryParams).filter(
38
+ (entry): entry is [string, string] => entry[1] != null,
39
+ ),
40
+ ).toString();
41
+ const slashes = isTripleSlashed ? '///' : '//';
42
+ const url = `${scheme}:${slashes}${cleanedPath}${query ? `?${query}` : ''}`;
43
+
44
+ if (preferLocalhost) {
45
+ const ipAddress = url.match(IPV4_PATTERN);
46
+ if (ipAddress?.length) {
47
+ const [protocol, rest] = url.split(ipAddress[0]);
48
+ return `${protocol}localhost${rest}`;
49
+ }
50
+ }
51
+ return url;
52
+ }
53
+
54
+ /** Builds an `AuthRequest` and loads its authorization URL before returning it. */
55
+ export async function loadAsync(
56
+ config: IAuthRequestConfig,
57
+ issuerOrDiscovery: IIssuerOrDiscovery,
58
+ ): Promise<AuthRequest> {
59
+ const request = new AuthRequest(config);
60
+ const discovery = await resolveDiscoveryAsync(issuerOrDiscovery);
61
+ await request.makeAuthUrlAsync(discovery);
62
+ return request;
63
+ }
@@ -0,0 +1,29 @@
1
+ import type { AuthError } from './errors';
2
+ import type { TokenResponse } from './token-request';
3
+
4
+ /** cancel/dismiss/opened/locked carry no extra data; error/success carry params + any token. */
5
+ export type IAuthSessionResult =
6
+ | { type: 'cancel' | 'dismiss' | 'opened' | 'locked' }
7
+ | {
8
+ type: 'error' | 'success';
9
+ /** @deprecated legacy error-code query param, use `error` instead. */
10
+ errorCode: string | null;
11
+ error?: AuthError | null;
12
+ params: Record<string, string>;
13
+ authentication: TokenResponse | null;
14
+ url: string;
15
+ };
16
+
17
+ export type IAuthSessionRedirectUriOptions = {
18
+ /** Appended to `scheme`; not applied when `native` is used. */
19
+ path?: string;
20
+ /** URI protocol `<scheme>://` built into the native app. Ignored if `native` is set. */
21
+ scheme?: string;
22
+ queryParams?: Record<string, string | undefined>;
23
+ /** `scheme:///path` when `true`, `scheme://path` otherwise. @default false */
24
+ isTripleSlashed?: boolean;
25
+ /** Rewrites a matched IPv4 host to `localhost` (iOS simulator convenience). @default false */
26
+ preferLocalhost?: boolean;
27
+ /** Manual scheme for a production build; takes precedence over every other option. */
28
+ native?: string;
29
+ };
@@ -0,0 +1,33 @@
1
+ const KEYSET =
2
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
3
+
4
+ export function encodeNoWrap(input: string): string {
5
+ let output = '';
6
+ let i = 0;
7
+
8
+ do {
9
+ const chr1 = input.charCodeAt(i++);
10
+ const chr2 = input.charCodeAt(i++);
11
+ const chr3 = input.charCodeAt(i++);
12
+
13
+ const enc1 = chr1 >> 2;
14
+ const enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
15
+ let enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
16
+ let enc4 = chr3 & 63;
17
+ if (isNaN(chr2)) {
18
+ enc3 = 64;
19
+ enc4 = 64;
20
+ } else if (isNaN(chr3)) {
21
+ enc4 = 64;
22
+ }
23
+
24
+ output =
25
+ output +
26
+ KEYSET.charAt(enc1) +
27
+ KEYSET.charAt(enc2) +
28
+ KEYSET.charAt(enc3) +
29
+ KEYSET.charAt(enc4);
30
+ } while (i < input.length);
31
+
32
+ return output;
33
+ }
@@ -0,0 +1,102 @@
1
+ import invariant from 'invariant';
2
+ import type { ICodeChallengeMethod } from './auth-request.types';
3
+ import { requestAsync } from './fetch';
4
+
5
+ /** URL using the `https` scheme with no query or fragment, asserted by the OP as its issuer. */
6
+ export type IIssuer = string;
7
+
8
+ export type IProviderMetadataEndpoints = {
9
+ issuer?: IIssuer;
10
+ authorization_endpoint: string;
11
+ token_endpoint: string;
12
+ userinfo_endpoint?: string;
13
+ revocation_endpoint?: string;
14
+ registration_endpoint?: string;
15
+ end_session_endpoint?: string;
16
+ introspection_endpoint?: string;
17
+ device_authorization_endpoint?: string;
18
+ };
19
+
20
+ export type IProviderMetadata = Record<string, string | boolean | string[]> &
21
+ IProviderMetadataEndpoints & {
22
+ jwks_uri?: string;
23
+ scopes_supported?: string[];
24
+ response_types_supported?: string[];
25
+ response_modes_supported?: string[];
26
+ grant_types_supported?: string[];
27
+ id_token_signing_alg_values_supported?: string[];
28
+ subject_types_supported?: string[];
29
+ token_endpoint_auth_methods_supported?: string[];
30
+ display_values_supported?: string[];
31
+ claim_types_supported?: string[];
32
+ claims_supported?: string[];
33
+ service_documentation?: string;
34
+ claims_locales_supported?: string[];
35
+ ui_locales_supported?: string[];
36
+ claims_parameter_supported?: boolean;
37
+ request_parameter_supported?: boolean;
38
+ request_uri_parameter_supported?: boolean;
39
+ require_request_uri_registration?: boolean;
40
+ op_policy_uri?: string;
41
+ op_tos_uri?: string;
42
+ code_challenge_methods_supported?: ICodeChallengeMethod[];
43
+ check_session_iframe?: string;
44
+ backchannel_logout_supported?: boolean;
45
+ backchannel_logout_session_supported?: boolean;
46
+ frontchannel_logout_supported?: boolean;
47
+ frontchannel_logout_session_supported?: boolean;
48
+ };
49
+
50
+ export type IDiscoveryDocument = {
51
+ authorizationEndpoint?: string;
52
+ tokenEndpoint?: string;
53
+ /** Some providers (e.g. Spotify) don't expose one despite the spec requiring it. */
54
+ revocationEndpoint?: string;
55
+ userInfoEndpoint?: string;
56
+ endSessionEndpoint?: string;
57
+ registrationEndpoint?: string;
58
+ discoveryDocument?: IProviderMetadata;
59
+ };
60
+
61
+ export type IIssuerOrDiscovery = IIssuer | IDiscoveryDocument;
62
+
63
+ /** https://tools.ietf.org/html/rfc5785 */
64
+ export function issuerWithWellKnownUrl(issuer: IIssuer): string {
65
+ return `${issuer}/.well-known/openid-configuration`;
66
+ }
67
+
68
+ export async function fetchDiscoveryAsync(
69
+ issuer: IIssuer,
70
+ ): Promise<IDiscoveryDocument> {
71
+ const json = await requestAsync<IProviderMetadata>(
72
+ issuerWithWellKnownUrl(issuer),
73
+ {
74
+ dataType: 'json',
75
+ method: 'GET',
76
+ },
77
+ );
78
+
79
+ return {
80
+ discoveryDocument: json,
81
+ authorizationEndpoint: json.authorization_endpoint,
82
+ tokenEndpoint: json.token_endpoint,
83
+ revocationEndpoint: json.revocation_endpoint,
84
+ userInfoEndpoint: json.userinfo_endpoint,
85
+ endSessionEndpoint: json.end_session_endpoint,
86
+ registrationEndpoint: json.registration_endpoint,
87
+ };
88
+ }
89
+
90
+ export async function resolveDiscoveryAsync(
91
+ issuerOrDiscovery: IIssuerOrDiscovery,
92
+ ): Promise<IDiscoveryDocument> {
93
+ invariant(
94
+ issuerOrDiscovery &&
95
+ !['number', 'boolean'].includes(typeof issuerOrDiscovery),
96
+ 'Expected a valid discovery object or issuer URL',
97
+ );
98
+ if (typeof issuerOrDiscovery === 'string') {
99
+ return await fetchDiscoveryAsync(issuerOrDiscovery);
100
+ }
101
+ return issuerOrDiscovery;
102
+ }
@@ -0,0 +1,102 @@
1
+ // Hand-ported from expo-modules-core's CodedError (src/errors/CodedError.ts) - this package
2
+ // depends on none of the rest of expo-modules-core, so a single small class is copied rather
3
+ // than pulling in the whole native-module runtime as a dependency.
4
+ class CodedError extends Error {
5
+ code: string;
6
+
7
+ constructor(code: string, message: string) {
8
+ super(message);
9
+ this.code = code;
10
+ }
11
+ }
12
+
13
+ export type IResponseErrorConfig = Record<string, unknown> & {
14
+ error: string;
15
+ error_description?: string;
16
+ error_uri?: string;
17
+ };
18
+
19
+ export type IAuthErrorConfig = IResponseErrorConfig & {
20
+ /** Required only if `state` was used in the initial request. */
21
+ state?: string;
22
+ };
23
+
24
+ const AUTH_ERROR_MESSAGES: Record<string, string> = {
25
+ invalid_request: `The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.`,
26
+ unauthorized_client: `The client is not authorized to request an authorization code using this method.`,
27
+ access_denied: `The resource owner or authorization server denied the request.`,
28
+ unsupported_response_type: `The authorization server does not support obtaining an authorization code using this method.`,
29
+ invalid_scope: 'The requested scope is invalid, unknown, or malformed.',
30
+ server_error:
31
+ 'The authorization server encountered an unexpected condition that prevented it from fulfilling the request.',
32
+ temporarily_unavailable:
33
+ 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
34
+ interaction_required:
35
+ 'Auth server requires user interaction of some form to proceed.',
36
+ login_required: 'Auth server requires user authentication.',
37
+ account_selection_required:
38
+ 'User is required to select a session at the auth server.',
39
+ consent_required: 'Auth server requires user consent.',
40
+ invalid_request_uri:
41
+ 'The `request_uri` in the auth request returns an error or contains invalid data.',
42
+ invalid_request_object:
43
+ 'The request parameter contains an invalid request object.',
44
+ request_not_supported:
45
+ 'The OP does not support use of the `request` parameter.',
46
+ request_uri_not_supported:
47
+ 'The OP does not support use of the `request_uri` parameter.',
48
+ registration_not_supported:
49
+ 'The OP does not support use of the `registration` parameter.',
50
+ };
51
+
52
+ const TOKEN_ERROR_MESSAGES: Record<string, string> = {
53
+ invalid_request: `The request is missing a required parameter, includes an unsupported parameter value (other than grant type), repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.`,
54
+ invalid_client: `Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method).`,
55
+ invalid_grant: `The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.`,
56
+ unauthorized_client: `The authenticated client is not authorized to use this authorization grant type.`,
57
+ unsupported_grant_type: `The authorization grant type is not supported by the authorization server.`,
58
+ };
59
+
60
+ /** https://tools.ietf.org/html/rfc6749#section-4.1.2.1 */
61
+ export class ResponseError extends CodedError {
62
+ description?: string;
63
+ uri?: string;
64
+ params: Record<string, unknown>;
65
+
66
+ constructor(params: IResponseErrorConfig, errorCodeType: 'auth' | 'token') {
67
+ const { error, error_description, error_uri } = params;
68
+ const message = (
69
+ errorCodeType === 'auth' ? AUTH_ERROR_MESSAGES : TOKEN_ERROR_MESSAGES
70
+ )[error];
71
+ let errorMessage: string;
72
+ if (message) {
73
+ errorMessage =
74
+ message +
75
+ (error_description ? `\nMore info: ${error_description}` : '');
76
+ } else if (error_description) {
77
+ errorMessage = error_description;
78
+ } else {
79
+ errorMessage = 'An unknown error occurred';
80
+ }
81
+ super(error, errorMessage);
82
+ this.description = error_description ?? message;
83
+ this.uri = error_uri;
84
+ this.params = params;
85
+ }
86
+ }
87
+
88
+ /** https://tools.ietf.org/html/rfc6749#section-5.2 */
89
+ export class AuthError extends ResponseError {
90
+ state?: string;
91
+
92
+ constructor(response: IAuthErrorConfig) {
93
+ super(response, 'auth');
94
+ this.state = response.state;
95
+ }
96
+ }
97
+
98
+ export class TokenError extends ResponseError {
99
+ constructor(response: IResponseErrorConfig) {
100
+ super(response, 'token');
101
+ }
102
+ }
@@ -0,0 +1,71 @@
1
+ export type IFetchHeaders = Record<string, string> & {
2
+ 'Content-Type': string;
3
+ Authorization?: string;
4
+ Accept?: string;
5
+ };
6
+
7
+ export type IFetchRequest = {
8
+ headers?: IFetchHeaders;
9
+ body?: Record<string, string>;
10
+ dataType?: string;
11
+ method?: string;
12
+ };
13
+
14
+ export function requestAsync<T>(
15
+ requestUrl: string,
16
+ fetchRequest: IFetchRequest & { dataType: 'json' },
17
+ ): Promise<T>;
18
+ export function requestAsync(
19
+ requestUrl: string,
20
+ fetchRequest: IFetchRequest,
21
+ ): Promise<string>;
22
+ export async function requestAsync<T>(
23
+ requestUrl: string,
24
+ fetchRequest: IFetchRequest,
25
+ ): Promise<T | string> {
26
+ const url = new URL(requestUrl);
27
+
28
+ const headers: Record<string, string> = {};
29
+ const request: RequestInit = {
30
+ body: undefined,
31
+ method: fetchRequest.method,
32
+ mode: 'cors',
33
+ headers,
34
+ };
35
+
36
+ const isJsonDataType = fetchRequest.dataType?.toLowerCase() === 'json';
37
+
38
+ if (fetchRequest.headers) {
39
+ for (const key in fetchRequest.headers) {
40
+ const header = fetchRequest.headers[key];
41
+ if (header != null) {
42
+ headers[key] = header;
43
+ }
44
+ }
45
+ }
46
+
47
+ if (fetchRequest.body) {
48
+ if (fetchRequest.method?.toUpperCase() === 'POST') {
49
+ request.body = new URLSearchParams(fetchRequest.body).toString();
50
+ } else {
51
+ for (const [key, value] of Object.entries(fetchRequest.body)) {
52
+ url.searchParams.append(key, value);
53
+ }
54
+ }
55
+ }
56
+
57
+ if (isJsonDataType && !headers.Accept && !headers.accept) {
58
+ headers['Accept'] = 'application/json, text/javascript; q=0.01';
59
+ }
60
+
61
+ // React Native's URL implementation adds a trailing slash that the server doesn't expect.
62
+ const correctedUrl = url.toString().replace(/\/$/, '');
63
+
64
+ const response = await fetch(correctedUrl, request);
65
+
66
+ const contentType = response.headers.get('content-type');
67
+ if (isJsonDataType || contentType?.includes('application/json')) {
68
+ return response.json();
69
+ }
70
+ return response.text();
71
+ }