@idealyst/oauth-client 1.0.69 → 1.0.70

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 CHANGED
@@ -1,15 +1,15 @@
1
1
  # @idealyst/oauth-client
2
2
 
3
- Universal OAuth2 client for web and React Native applications with a single API.
3
+ Universal OAuth2 client for web and React Native applications with minimal server requirements.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - 🌐 **Universal**: Works on both web and React Native with the same API
8
- - 🔐 **Secure**: Uses PKCE for mobile, supports client secrets for web
8
+ - 🖥️ **Minimal Server**: Server only needs to redirect - no token handling required
9
+ - 🔗 **Deep Link Support**: Handles mobile OAuth callbacks via custom URL schemes
10
+ - 🔐 **Secure**: Uses PKCE flow, client exchanges tokens directly with OAuth provider
9
11
  - 🏪 **Storage**: Automatic token storage with customizable adapters
10
- - 🔄 **Refresh**: Automatic token refresh handling
11
- - 🚪 **Logout**: Proper logout with token revocation
12
- - 📱 **Mobile**: Uses `react-native-app-auth` for secure system browser flow
12
+ - 🔄 **Refresh**: Direct token refresh with OAuth provider
13
13
  - 🎯 **TypeScript**: Fully typed for better developer experience
14
14
 
15
15
  ## Installation
@@ -24,8 +24,7 @@ yarn add @idealyst/oauth-client
24
24
 
25
25
  #### For React Native:
26
26
  ```bash
27
- npm install react-native-app-auth @react-native-async-storage/async-storage
28
- # Follow react-native-app-auth setup instructions for iOS/Android
27
+ npm install @react-native-async-storage/async-storage
29
28
  ```
30
29
 
31
30
  #### For Web:
@@ -34,47 +33,130 @@ No additional dependencies required.
34
33
  ## Quick Start
35
34
 
36
35
  ```typescript
37
- import { createOAuthClient, providers } from '@idealyst/oauth-client'
36
+ import { createOAuthClient } from '@idealyst/oauth-client'
38
37
 
39
- // Create OAuth client (works on both web and mobile)
40
38
  const client = createOAuthClient({
41
- ...providers.google,
39
+ apiBaseUrl: 'https://api.yourapp.com',
40
+ provider: 'google', // Your server endpoint: /auth/google (just redirects)
41
+ redirectUrl: 'com.yourapp://oauth/callback',
42
+
43
+ // OAuth provider config for direct token exchange
44
+ issuer: 'https://accounts.google.com',
42
45
  clientId: 'your-google-client-id',
43
- redirectUrl: 'com.yourapp://oauth/callback', // Mobile
44
- // redirectUrl: 'http://localhost:3000/auth/callback', // Web
46
+
47
+ scopes: ['profile', 'email'],
45
48
  })
46
49
 
47
- // Authorize user - same API on web and mobile!
48
- try {
49
- const result = await client.authorize()
50
- console.log('Access token:', result.tokens.accessToken)
51
- console.log('User data:', result.user)
52
- } catch (error) {
53
- console.error('Authorization failed:', error)
54
- }
50
+ // Works on both web and mobile!
51
+ const result = await client.authorize()
52
+ console.log('Access token:', result.tokens.accessToken)
53
+ ```
54
+
55
+ ## How It Works
56
+
57
+ This library uses a hybrid approach that minimizes server requirements:
58
+
59
+ ### Web Flow:
60
+ 1. Client redirects to `GET /api/auth/google?redirect_uri=com.yourapp://oauth/callback&state=xyz&code_challenge=abc`
61
+ 2. Server redirects to Google OAuth with server's client credentials + client's PKCE challenge
62
+ 3. Google redirects back to `com.yourapp://oauth/callback?code=123&state=xyz`
63
+ 4. Client automatically detects callback and exchanges code directly with Google using PKCE
64
+
65
+ ### Mobile Flow:
66
+ 1. App opens browser to `GET /api/auth/google?redirect_uri=com.yourapp://oauth/callback&state=xyz&code_challenge=abc`
67
+ 2. Server redirects to Google OAuth with server's client credentials + client's PKCE challenge
68
+ 3. Google redirects back to `com.yourapp://oauth/callback?code=123&state=xyz`
69
+ 4. Mobile OS opens app via deep link, client exchanges code directly with Google using PKCE
70
+
71
+ ## Minimal Server Setup
72
+
73
+ Your server only needs **ONE simple endpoint**:
74
+
75
+ ### GET `/auth/{provider}` - OAuth Redirect
76
+
77
+ ```javascript
78
+ app.get('/auth/:provider', (req, res) => {
79
+ const { redirect_uri, state, scope, code_challenge, code_challenge_method } = req.query
80
+
81
+ // Build OAuth URL with your server's credentials + client's PKCE
82
+ const authUrl = buildOAuthUrl(req.params.provider, {
83
+ client_id: process.env.GOOGLE_CLIENT_ID,
84
+ redirect_uri: redirect_uri, // Client's redirect URI
85
+ state: state, // Client's state for CSRF protection
86
+ scope: scope || 'profile email',
87
+ response_type: 'code',
88
+ code_challenge: code_challenge, // Client's PKCE challenge
89
+ code_challenge_method: code_challenge_method || 'S256',
90
+ })
91
+
92
+ res.redirect(authUrl)
93
+ })
94
+ ```
95
+
96
+ That's it! No token exchange, no callbacks, no database - just a simple redirect.
97
+
98
+ ## React Native Setup
99
+
100
+ ### iOS Configuration
101
+
102
+ Add URL scheme to your `Info.plist`:
103
+
104
+ ```xml
105
+ <key>CFBundleURLTypes</key>
106
+ <array>
107
+ <dict>
108
+ <key>CFBundleURLName</key>
109
+ <string>com.yourapp.oauth</string>
110
+ <key>CFBundleURLSchemes</key>
111
+ <array>
112
+ <string>com.yourapp</string>
113
+ </array>
114
+ </dict>
115
+ </array>
55
116
  ```
56
117
 
118
+ ### Android Configuration
119
+
120
+ Add intent filter to `android/app/src/main/AndroidManifest.xml`:
121
+
122
+ ```xml
123
+ <activity android:name=".MainActivity">
124
+ <intent-filter android:label="oauth_callback">
125
+ <action android:name="android.intent.action.VIEW" />
126
+ <category android:name="android.intent.category.DEFAULT" />
127
+ <category android:name="android.intent.category.BROWSABLE" />
128
+ <data android:scheme="com.yourapp" />
129
+ </intent-filter>
130
+ </activity>
131
+ ```
132
+
133
+ ### Deep Link Handling (Automatic)
134
+
135
+ The client automatically handles OAuth deep links. The deep link handler is built-in and requires no additional setup.
136
+
57
137
  ## API Reference
58
138
 
59
139
  ### createOAuthClient(config, storage?)
60
140
 
61
- Creates a platform-specific OAuth client with a unified API.
62
-
63
141
  ```typescript
64
142
  const client = createOAuthClient({
143
+ // Your server (just redirects)
144
+ apiBaseUrl: 'https://api.yourapp.com',
145
+ provider: 'google',
146
+ redirectUrl: 'com.yourapp://oauth/callback',
147
+
148
+ // OAuth provider config (for direct token exchange)
65
149
  issuer: 'https://accounts.google.com',
66
- clientId: 'your-client-id',
67
- redirectUrl: 'your-app://oauth',
68
- scopes: ['openid', 'profile', 'email'],
150
+ clientId: 'your-google-client-id',
151
+ tokenEndpoint: 'https://oauth2.googleapis.com/token', // Optional
69
152
 
70
153
  // Optional
154
+ scopes: ['profile', 'email'],
71
155
  additionalParameters: { prompt: 'consent' },
72
- customHeaders: { 'X-Custom': 'value' },
156
+ customHeaders: { 'Authorization': 'Bearer api-key' },
73
157
  })
74
158
  ```
75
159
 
76
- **⚠️ Security**: Client secrets are **never** used in this library. All flows use PKCE for security, which is the OAuth 2.1 standard for public clients.
77
-
78
160
  ### OAuthClient Methods
79
161
 
80
162
  #### authorize()
@@ -83,18 +165,17 @@ Initiates the OAuth flow and returns tokens.
83
165
  ```typescript
84
166
  const result = await client.authorize()
85
167
  // result.tokens: { accessToken, refreshToken, idToken, expiresAt, ... }
86
- // result.user: Additional user data (provider-specific)
87
168
  ```
88
169
 
89
170
  #### refresh(refreshToken)
90
- Refreshes an expired access token.
171
+ Refreshes an expired access token directly with OAuth provider.
91
172
 
92
173
  ```typescript
93
174
  const result = await client.refresh(refreshToken)
94
175
  ```
95
176
 
96
177
  #### getStoredTokens()
97
- Retrieves stored tokens from storage.
178
+ Retrieves stored tokens from local storage.
98
179
 
99
180
  ```typescript
100
181
  const tokens = await client.getStoredTokens()
@@ -103,179 +184,80 @@ if (tokens?.expiresAt && tokens.expiresAt < new Date()) {
103
184
  }
104
185
  ```
105
186
 
106
- #### revoke(token)
107
- Revokes a specific token.
108
-
109
- ```typescript
110
- await client.revoke(accessToken)
111
- ```
112
-
113
- #### logout()
114
- Logs out the user, revokes tokens, and clears storage.
115
-
116
- ```typescript
117
- await client.logout()
118
- ```
119
-
120
187
  #### clearStoredTokens()
121
- Manually clears stored tokens.
188
+ Clears stored tokens from local storage.
122
189
 
123
190
  ```typescript
124
191
  await client.clearStoredTokens()
125
192
  ```
126
193
 
127
- ## Provider Configurations
128
-
129
- Pre-configured settings for popular OAuth providers:
130
-
131
- ```typescript
132
- import { providers } from '@idealyst/oauth-client'
133
-
134
- // Google
135
- const googleClient = createOAuthClient({
136
- ...providers.google,
137
- clientId: 'your-google-client-id',
138
- redirectUrl: 'your-redirect-url',
139
- })
140
-
141
- // GitHub
142
- const githubClient = createOAuthClient({
143
- ...providers.github,
144
- clientId: 'your-github-client-id',
145
- redirectUrl: 'your-redirect-url',
146
- })
147
-
148
- // Microsoft
149
- const msClient = createOAuthClient({
150
- ...providers.microsoft,
151
- clientId: 'your-microsoft-client-id',
152
- redirectUrl: 'your-redirect-url',
153
- })
154
-
155
- // Auth0
156
- const auth0Client = createOAuthClient({
157
- ...providers.auth0('your-domain.auth0.com'),
158
- clientId: 'your-auth0-client-id',
159
- redirectUrl: 'your-redirect-url',
160
- })
161
-
162
- // Okta
163
- const oktaClient = createOAuthClient({
164
- ...providers.okta('dev-123.okta.com'),
165
- clientId: 'your-okta-client-id',
166
- redirectUrl: 'your-redirect-url',
167
- })
168
- ```
169
-
170
- ## Custom Storage
171
-
172
- By default, the library uses `localStorage` on web and `AsyncStorage` on mobile. You can provide a custom storage adapter:
194
+ #### logout()
195
+ Clears stored tokens (server handles actual logout/revocation).
173
196
 
174
197
  ```typescript
175
- import { createOAuthClient } from '@idealyst/oauth-client'
176
-
177
- const customStorage = {
178
- async getItem(key: string): Promise<string | null> {
179
- // Your storage implementation
180
- },
181
- async setItem(key: string, value: string): Promise<void> {
182
- // Your storage implementation
183
- },
184
- async removeItem(key: string): Promise<void> {
185
- // Your storage implementation
186
- },
187
- }
188
-
189
- const client = createOAuthClient(config, customStorage)
198
+ await client.logout()
190
199
  ```
191
200
 
192
- ## Platform-Specific Configuration
193
-
194
- ### Web Configuration
201
+ ## Provider Examples
195
202
 
196
- For web applications, use standard HTTP redirect URLs:
203
+ ### Google OAuth
197
204
 
198
205
  ```typescript
199
- const webClient = createOAuthClient({
200
- ...providers.google,
206
+ const client = createOAuthClient({
207
+ apiBaseUrl: 'https://api.yourapp.com',
208
+ provider: 'google',
209
+ redirectUrl: 'com.yourapp://oauth/callback',
210
+
211
+ issuer: 'https://accounts.google.com',
201
212
  clientId: 'your-google-client-id',
202
- redirectUrl: 'https://yourapp.com/auth/callback',
213
+ scopes: ['profile', 'email'],
203
214
  })
204
215
  ```
205
216
 
206
- **⚠️ Security Note**: Client secrets should **NEVER** be included in client-side code. This library uses PKCE (Proof Key for Code Exchange) which provides security for public clients without requiring client secrets.
207
-
208
- ### Mobile Configuration
209
-
210
- For mobile apps, you need to:
211
-
212
- 1. **Configure custom URL scheme** in your app
213
- 2. **Register the URL scheme** with your OAuth provider
214
- 3. **Use the custom URL** as redirect URL
217
+ ### GitHub OAuth
215
218
 
216
219
  ```typescript
217
- const mobileClient = createOAuthClient({
218
- ...providers.google,
219
- clientId: 'your-google-client-id',
220
+ const client = createOAuthClient({
221
+ apiBaseUrl: 'https://api.yourapp.com',
222
+ provider: 'github',
220
223
  redirectUrl: 'com.yourapp://oauth/callback',
224
+
225
+ issuer: 'https://github.com',
226
+ clientId: 'your-github-client-id',
227
+ tokenEndpoint: 'https://github.com/login/oauth/access_token',
228
+ scopes: ['user', 'user:email'],
221
229
  })
222
230
  ```
223
231
 
224
- ## React Native Setup
232
+ ## Token Management
225
233
 
226
- ### iOS Configuration
227
-
228
- 1. Add URL scheme to your `Info.plist`:
229
-
230
- ```xml
231
- <key>CFBundleURLTypes</key>
232
- <array>
233
- <dict>
234
- <key>CFBundleURLName</key>
235
- <string>com.yourapp.oauth</string>
236
- <key>CFBundleURLSchemes</key>
237
- <array>
238
- <string>com.yourapp</string>
239
- </array>
240
- </dict>
241
- </array>
242
- ```
243
-
244
- ### Android Configuration
245
-
246
- 1. Add intent filter to `android/app/src/main/AndroidManifest.xml`:
234
+ ```typescript
235
+ async function getValidTokens() {
236
+ const storedTokens = await client.getStoredTokens()
237
+
238
+ if (storedTokens) {
239
+ // Check if expired
240
+ if (storedTokens.expiresAt && storedTokens.expiresAt < new Date()) {
241
+ if (storedTokens.refreshToken) {
242
+ try {
243
+ const refreshed = await client.refresh(storedTokens.refreshToken)
244
+ return refreshed.tokens
245
+ } catch (error) {
246
+ // Refresh failed, need to re-authenticate
247
+ await client.clearStoredTokens()
248
+ }
249
+ }
250
+ } else {
251
+ return storedTokens
252
+ }
253
+ }
247
254
 
248
- ```xml
249
- <activity android:name=".MainActivity">
250
- <intent-filter android:label="filter_react_native">
251
- <action android:name="android.intent.action.VIEW" />
252
- <category android:name="android.intent.category.DEFAULT" />
253
- <category android:name="android.intent.category.BROWSABLE" />
254
- <data android:scheme="com.yourapp" />
255
- </intent-filter>
256
- </activity>
255
+ // No valid tokens, start OAuth flow
256
+ const result = await client.authorize()
257
+ return result.tokens
258
+ }
257
259
  ```
258
260
 
259
- 2. Follow the [react-native-app-auth setup guide](https://github.com/FormidableLabs/react-native-app-auth#setup) for additional configuration.
260
-
261
- ### OAuth Provider Setup
262
-
263
- Register your custom URL scheme as a valid redirect URI in your OAuth provider:
264
-
265
- - **Google**: Add `com.yourapp://oauth/callback` to "Authorized redirect URIs"
266
- - **GitHub**: Set "Authorization callback URL" to `com.yourapp://oauth/callback`
267
- - **Other providers**: Add the URL scheme to allowed callback URLs
268
-
269
- ## How Mobile OAuth Works
270
-
271
- 1. **App opens system browser** (Safari/Chrome) with OAuth URL
272
- 2. **User authenticates** in the browser (can use saved passwords, Touch ID, etc.)
273
- 3. **Provider redirects** to your custom URL scheme (`com.yourapp://oauth/callback`)
274
- 4. **OS recognizes the scheme** and opens your app
275
- 5. **react-native-app-auth** automatically extracts tokens and returns them
276
-
277
- This provides the most secure OAuth flow for mobile apps, as recommended by OAuth 2.0 security best practices.
278
-
279
261
  ## Error Handling
280
262
 
281
263
  ```typescript
@@ -284,21 +266,29 @@ try {
284
266
  } catch (error) {
285
267
  if (error.message.includes('User cancelled')) {
286
268
  // User cancelled the authorization
287
- } else if (error.message.includes('network')) {
288
- // Network error
269
+ } 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)
289
273
  } else {
290
274
  // Other OAuth error
291
275
  }
292
276
  }
293
277
  ```
294
278
 
295
- ## TypeScript
279
+ ## Security Benefits
296
280
 
297
- The library is fully typed. Import types as needed:
281
+ **No client secrets in client code** - Only client ID needed
282
+ ✅ **PKCE protection** - Secure code exchange without client secrets
283
+ ✅ **CSRF protection** - Uses state parameter
284
+ ✅ **Direct token exchange** - Client communicates directly with OAuth provider
285
+ ✅ **Minimal server attack surface** - Server only redirects, doesn't handle tokens
286
+
287
+ ## TypeScript
298
288
 
299
289
  ```typescript
300
290
  import type {
301
- OAuthConfig,
291
+ ServerOAuthConfig,
302
292
  OAuthTokens,
303
293
  OAuthResult,
304
294
  OAuthClient
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/oauth-client",
3
- "version": "1.0.69",
3
+ "version": "1.0.70",
4
4
  "description": "Universal OAuth2 client for web and React Native",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -30,7 +30,7 @@
30
30
  "author": "",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "react-native-app-auth": "^7.2.0"
33
+ "@idealyst/storage": "1.0.70"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/react": "^18.3.18",
@@ -41,13 +41,7 @@
41
41
  "typescript": "^5.7.3"
42
42
  },
43
43
  "peerDependencies": {
44
- "react": ">=18",
45
- "react-native": ">=0.72"
46
- },
47
- "peerDependenciesMeta": {
48
- "react-native": {
49
- "optional": true
50
- }
44
+ "@idealyst/storage": "1.0.70"
51
45
  },
52
46
  "files": [
53
47
  "dist",
@@ -0,0 +1,60 @@
1
+ // Import will resolve to index.web.ts or index.native.ts based on platform
2
+ import { createOAuthClient } from '../index'
3
+
4
+ // Example: Simple OAuth flow (works on both web and mobile)
5
+ export async function setupGoogleOAuth() {
6
+ const client = createOAuthClient({
7
+ oauthUrl: 'https://api.yourapp.com/auth/google', // Your server endpoint
8
+ redirectUrl: 'com.yourapp://oauth/callback', // Same for web and mobile
9
+ additionalParameters: {
10
+ scope: 'profile email'
11
+ }
12
+ })
13
+
14
+ try {
15
+ // Works on both web and mobile:
16
+ // 1. Redirects to your server endpoint
17
+ // 2. Server redirects to Google OAuth
18
+ // 3. Google redirects back to com.yourapp://oauth/callback?code=xyz
19
+ // 4. Client returns the code
20
+ const result = await client.authorize()
21
+
22
+ console.log('Authorization code:', result.code)
23
+ console.log('State:', result.state)
24
+
25
+ // Now you can exchange the code for tokens on your server
26
+ return result.code
27
+ } catch (error) {
28
+ console.error('OAuth error:', error)
29
+ throw error
30
+ }
31
+ }
32
+
33
+ // Example: GitHub OAuth
34
+ export async function setupGitHubOAuth() {
35
+ const client = createOAuthClient({
36
+ oauthUrl: 'https://api.yourapp.com/auth/github',
37
+ redirectUrl: 'com.yourapp://oauth/callback',
38
+ additionalParameters: {
39
+ scope: 'user user:email'
40
+ }
41
+ })
42
+
43
+ const result = await client.authorize()
44
+ return result.code
45
+ }
46
+
47
+ // Example: Custom OAuth provider
48
+ export async function setupCustomOAuth() {
49
+ const client = createOAuthClient({
50
+ oauthUrl: 'https://api.yourapp.com/auth/custom-provider',
51
+ redirectUrl: 'com.yourapp://oauth/callback',
52
+ additionalParameters: {
53
+ scope: 'read write',
54
+ prompt: 'consent'
55
+ }
56
+ })
57
+
58
+ const result = await client.authorize()
59
+ return result.code
60
+ }
@@ -0,0 +1,10 @@
1
+ export * from './types'
2
+
3
+ import type { OAuthConfig, OAuthClient } from './types'
4
+ import { NativeOAuthClient } from './oauth-client.native'
5
+
6
+ export function createOAuthClient(config: OAuthConfig): OAuthClient {
7
+ return new NativeOAuthClient(config)
8
+ }
9
+
10
+ export { NativeOAuthClient }
package/src/index.ts CHANGED
@@ -1,55 +1,10 @@
1
+ // This file should not be used - use index.web.ts or index.native.ts instead
1
2
  export * from './types'
2
- export * from './storage'
3
3
 
4
- import type { OAuthConfig, OAuthClient, StorageAdapter } from './types'
5
- import { WebOAuthClient } from './web-client'
6
- import { NativeOAuthClient } from './native-client'
7
- import { createDefaultStorage } from './storage'
8
-
9
- export function createOAuthClient(
10
- config: OAuthConfig,
11
- storage?: StorageAdapter
12
- ): OAuthClient {
13
- const storageAdapter = storage || createDefaultStorage()
14
-
15
- // Check if we're in React Native environment
16
- if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
17
- return new NativeOAuthClient(config, storageAdapter)
18
- }
19
-
20
- // Default to web client
21
- return new WebOAuthClient(config, storageAdapter)
22
- }
23
-
24
- // Common provider configurations
25
- export const providers = {
26
- google: {
27
- issuer: 'https://accounts.google.com',
28
- scopes: ['openid', 'profile', 'email'],
29
- },
30
-
31
- github: {
32
- issuer: 'https://github.com',
33
- authorizationEndpoint: 'https://github.com/login/oauth/authorize',
34
- tokenEndpoint: 'https://github.com/login/oauth/access_token',
35
- scopes: ['user'],
36
- },
37
-
38
- microsoft: {
39
- issuer: 'https://login.microsoftonline.com/common/v2.0',
40
- scopes: ['openid', 'profile', 'email'],
41
- },
42
-
43
- auth0: (domain: string) => ({
44
- issuer: `https://${domain}`,
45
- scopes: ['openid', 'profile', 'email'],
46
- }),
47
-
48
- okta: (domain: string) => ({
49
- issuer: `https://${domain}`,
50
- scopes: ['openid', 'profile', 'email'],
51
- }),
52
- } as const
53
-
54
- export { WebOAuthClient } from './web-client'
55
- export { NativeOAuthClient } from './native-client'
4
+ // Fallback export that throws an error
5
+ export function createOAuthClient(): never {
6
+ throw new Error(
7
+ '@idealyst/oauth-client: Platform-specific bundle not found. ' +
8
+ 'Make sure your bundler is configured to use .web.ts or .native.ts files.'
9
+ )
10
+ }
@@ -0,0 +1,10 @@
1
+ export * from './types'
2
+
3
+ import type { OAuthConfig, OAuthClient } from './types'
4
+ import { WebOAuthClient } from './oauth-client.web'
5
+
6
+ export function createOAuthClient(config: OAuthConfig): OAuthClient {
7
+ return new WebOAuthClient(config)
8
+ }
9
+
10
+ export { WebOAuthClient }