@idealyst/oauth-client 1.2.111 → 1.2.112
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.
- package/README.md +16 -11
- package/package.json +4 -2
- package/src/oauth-client.native.ts +20 -66
package/README.md
CHANGED
|
@@ -24,9 +24,13 @@ yarn add @idealyst/oauth-client
|
|
|
24
24
|
|
|
25
25
|
#### For React Native:
|
|
26
26
|
```bash
|
|
27
|
-
npm install
|
|
27
|
+
npm install expo-web-browser
|
|
28
|
+
# Then for iOS:
|
|
29
|
+
cd ios && pod install
|
|
28
30
|
```
|
|
29
31
|
|
|
32
|
+
This provides `ASWebAuthenticationSession` on iOS (in-app auth sheet, no redirect prompt) and `Chrome Custom Tabs` on Android (in-app browser overlay). Works in bare React Native projects — requires the `expo` core package.
|
|
33
|
+
|
|
30
34
|
#### For Web:
|
|
31
35
|
No additional dependencies required.
|
|
32
36
|
|
|
@@ -63,10 +67,11 @@ This library uses a hybrid approach that minimizes server requirements:
|
|
|
63
67
|
4. Client automatically detects callback and exchanges code directly with Google using PKCE
|
|
64
68
|
|
|
65
69
|
### Mobile Flow:
|
|
66
|
-
1. App opens
|
|
67
|
-
2.
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
70
|
+
1. App opens an in-app auth session (`ASWebAuthenticationSession` on iOS, `Chrome Custom Tabs` on Android)
|
|
71
|
+
2. Auth session navigates to `GET /api/auth/google?redirect_uri=com.yourapp://oauth/callback&state=xyz&code_challenge=abc`
|
|
72
|
+
3. Server redirects to Google OAuth with server's client credentials + client's PKCE challenge
|
|
73
|
+
4. Google redirects back to `com.yourapp://oauth/callback?code=123&state=xyz`
|
|
74
|
+
5. Auth session captures the redirect and returns the URL directly to the app (no system prompt on iOS)
|
|
70
75
|
|
|
71
76
|
## Minimal Server Setup
|
|
72
77
|
|
|
@@ -130,9 +135,9 @@ Add intent filter to `android/app/src/main/AndroidManifest.xml`:
|
|
|
130
135
|
</activity>
|
|
131
136
|
```
|
|
132
137
|
|
|
133
|
-
###
|
|
138
|
+
### In-App Auth Session (Automatic)
|
|
134
139
|
|
|
135
|
-
The client
|
|
140
|
+
The client uses `expo-web-browser` to handle the OAuth flow within an in-app browser session. On iOS this uses `ASWebAuthenticationSession` which avoids the "Open in App?" system prompt that occurs with Safari redirects. On Android it uses Chrome Custom Tabs. No manual deep link listener setup is required.
|
|
136
141
|
|
|
137
142
|
## API Reference
|
|
138
143
|
|
|
@@ -265,11 +270,11 @@ try {
|
|
|
265
270
|
const result = await client.authorize()
|
|
266
271
|
} catch (error) {
|
|
267
272
|
if (error.message.includes('User cancelled')) {
|
|
268
|
-
// User
|
|
273
|
+
// User dismissed the auth session
|
|
274
|
+
} else if (error.message.includes('not available')) {
|
|
275
|
+
// InAppBrowser not available on device (missing native dependency)
|
|
269
276
|
} else if (error.message.includes('Invalid state')) {
|
|
270
|
-
// CSRF protection triggered
|
|
271
|
-
} else if (error.message.includes('timeout')) {
|
|
272
|
-
// User didn't complete OAuth in time (mobile)
|
|
277
|
+
// CSRF protection triggered
|
|
273
278
|
} else {
|
|
274
279
|
// Other OAuth error
|
|
275
280
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/oauth-client",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.112",
|
|
4
4
|
"description": "Universal OAuth2 client for web and React Native",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
"@types/react-native": "^0.73.0",
|
|
39
39
|
"react": "^19.1.0",
|
|
40
40
|
"react-native": "^0.80.1",
|
|
41
|
+
"expo-web-browser": "^15.0.0",
|
|
41
42
|
"tsup": "^8.3.5",
|
|
42
43
|
"typescript": "^5.7.3"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
46
|
"@idealyst/storage": "^1.2.30",
|
|
46
|
-
"react-native": ">=0.60.0"
|
|
47
|
+
"react-native": ">=0.60.0",
|
|
48
|
+
"expo-web-browser": ">=13.0.0"
|
|
47
49
|
},
|
|
48
50
|
"files": [
|
|
49
51
|
"dist",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OAuthClient, OAuthConfig, OAuthResult, OAuthCallbackParams } from './types'
|
|
2
|
-
import
|
|
2
|
+
import * as WebBrowser from 'expo-web-browser'
|
|
3
3
|
|
|
4
4
|
export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
|
|
5
5
|
private config: OAuthConfig<T>
|
|
@@ -12,11 +12,22 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
|
|
|
12
12
|
const state = this.generateState()
|
|
13
13
|
const oauthUrl = this.buildOAuthUrl(state)
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
await
|
|
15
|
+
// Use expo-web-browser's auth session (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android)
|
|
16
|
+
const result = await WebBrowser.openAuthSessionAsync(oauthUrl, this.config.redirectUrl)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (result.type === 'cancel' || result.type === 'dismiss') {
|
|
19
|
+
throw new Error('User cancelled the authorization')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (result.type !== 'success' || !result.url) {
|
|
23
|
+
throw new Error('OAuth flow failed unexpectedly')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const callbackParams = this.parseRedirectUrl(result.url)
|
|
27
|
+
|
|
28
|
+
if (!callbackParams) {
|
|
29
|
+
throw new Error('Failed to parse OAuth callback parameters')
|
|
30
|
+
}
|
|
20
31
|
|
|
21
32
|
if (callbackParams.error) {
|
|
22
33
|
throw new Error(`OAuth error: ${callbackParams.error}`)
|
|
@@ -31,67 +42,10 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
|
|
|
31
42
|
return callbackParams as T
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
private
|
|
35
|
-
return new Promise((resolve, reject) => {
|
|
36
|
-
let subscription: any
|
|
37
|
-
let timeoutId: NodeJS.Timeout | null = null
|
|
38
|
-
|
|
39
|
-
const handleUrl = (event: { url: string }) => {
|
|
40
|
-
const callbackData = this.parseDeepLink(event.url)
|
|
41
|
-
if (callbackData) {
|
|
42
|
-
cleanup()
|
|
43
|
-
resolve(callbackData)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const cleanup = () => {
|
|
48
|
-
if (subscription?.remove) {
|
|
49
|
-
subscription.remove()
|
|
50
|
-
} else if (subscription) {
|
|
51
|
-
// For newer React Native versions
|
|
52
|
-
subscription()
|
|
53
|
-
}
|
|
54
|
-
if (timeoutId) {
|
|
55
|
-
clearTimeout(timeoutId)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Check for initial URL (if app was opened from deep link)
|
|
60
|
-
Linking.getInitialURL().then((url: string | null) => {
|
|
61
|
-
if (url) {
|
|
62
|
-
const callbackData = this.parseDeepLink(url)
|
|
63
|
-
if (callbackData) {
|
|
64
|
-
cleanup()
|
|
65
|
-
resolve(callbackData)
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}).catch((error: Error) => {
|
|
70
|
-
console.warn('Failed to get initial URL:', error)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Listen for subsequent deep links
|
|
74
|
-
subscription = Linking.addEventListener('url', handleUrl)
|
|
75
|
-
|
|
76
|
-
// Timeout after 5 minutes
|
|
77
|
-
timeoutId = setTimeout(() => {
|
|
78
|
-
cleanup()
|
|
79
|
-
reject(new Error('OAuth timeout - user did not complete authorization'))
|
|
80
|
-
}, 5 * 60 * 1000)
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private parseDeepLink(url: string): OAuthCallbackParams | null {
|
|
45
|
+
private parseRedirectUrl(url: string): OAuthCallbackParams | null {
|
|
85
46
|
try {
|
|
86
|
-
// Handle custom scheme URLs (e.g., com.myapp://oauth/callback?code=123)
|
|
87
47
|
const parsedUrl = new URL(url)
|
|
88
48
|
|
|
89
|
-
// Check if this is our OAuth callback
|
|
90
|
-
const expectedScheme = new URL(this.config.redirectUrl).protocol.slice(0, -1)
|
|
91
|
-
if (parsedUrl.protocol.slice(0, -1) !== expectedScheme) {
|
|
92
|
-
return null
|
|
93
|
-
}
|
|
94
|
-
|
|
95
49
|
// Collect all query parameters
|
|
96
50
|
const params: OAuthCallbackParams = {}
|
|
97
51
|
|
|
@@ -115,14 +69,14 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
|
|
|
115
69
|
|
|
116
70
|
return params
|
|
117
71
|
} catch (error) {
|
|
118
|
-
console.warn('Failed to parse
|
|
72
|
+
console.warn('Failed to parse redirect URL:', url, error)
|
|
119
73
|
return null
|
|
120
74
|
}
|
|
121
75
|
}
|
|
122
76
|
|
|
123
77
|
private buildOAuthUrl(state: string): string {
|
|
124
78
|
const url = new URL(this.config.oauthUrl)
|
|
125
|
-
|
|
79
|
+
|
|
126
80
|
url.searchParams.set('redirect_uri', this.config.redirectUrl)
|
|
127
81
|
url.searchParams.set('state', state)
|
|
128
82
|
|
|
@@ -145,4 +99,4 @@ export class NativeOAuthClient<T = OAuthResult> implements OAuthClient<T> {
|
|
|
145
99
|
}
|
|
146
100
|
return result
|
|
147
101
|
}
|
|
148
|
-
}
|
|
102
|
+
}
|