@flink-app/oauth-plugin 0.12.1-alpha.33
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/LICENSE +21 -0
- package/README.md +783 -0
- package/SECURITY.md +433 -0
- package/dist/OAuthInternalContext.d.ts +45 -0
- package/dist/OAuthInternalContext.js +2 -0
- package/dist/OAuthPlugin.d.ts +70 -0
- package/dist/OAuthPlugin.js +220 -0
- package/dist/OAuthPluginContext.d.ts +49 -0
- package/dist/OAuthPluginContext.js +2 -0
- package/dist/OAuthPluginOptions.d.ts +111 -0
- package/dist/OAuthPluginOptions.js +2 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +66 -0
- package/dist/providers/GitHubProvider.d.ts +32 -0
- package/dist/providers/GitHubProvider.js +82 -0
- package/dist/providers/GoogleProvider.d.ts +32 -0
- package/dist/providers/GoogleProvider.js +83 -0
- package/dist/providers/OAuthProvider.d.ts +69 -0
- package/dist/providers/OAuthProvider.js +2 -0
- package/dist/providers/OAuthProviderBase.d.ts +32 -0
- package/dist/providers/OAuthProviderBase.js +86 -0
- package/dist/providers/ProviderRegistry.d.ts +14 -0
- package/dist/providers/ProviderRegistry.js +24 -0
- package/dist/repos/OAuthConnectionRepo.d.ts +30 -0
- package/dist/repos/OAuthConnectionRepo.js +38 -0
- package/dist/repos/OAuthSessionRepo.d.ts +22 -0
- package/dist/repos/OAuthSessionRepo.js +28 -0
- package/dist/schemas/OAuthConnection.d.ts +12 -0
- package/dist/schemas/OAuthConnection.js +2 -0
- package/dist/schemas/OAuthSession.d.ts +9 -0
- package/dist/schemas/OAuthSession.js +2 -0
- package/dist/utils/encryption-utils.d.ts +34 -0
- package/dist/utils/encryption-utils.js +134 -0
- package/dist/utils/error-utils.d.ts +68 -0
- package/dist/utils/error-utils.js +120 -0
- package/dist/utils/state-utils.d.ts +36 -0
- package/dist/utils/state-utils.js +72 -0
- package/examples/api-client-auth.ts +550 -0
- package/examples/basic-auth.ts +288 -0
- package/examples/multi-provider.ts +409 -0
- package/examples/token-storage.ts +490 -0
- package/package.json +38 -0
- package/spec/OAuthHandlers.spec.ts +146 -0
- package/spec/OAuthPluginSpec.ts +31 -0
- package/spec/ProvidersSpec.ts +178 -0
- package/spec/README.md +365 -0
- package/spec/helpers/mockJwtAuthPlugin.ts +104 -0
- package/spec/helpers/mockOAuthProviders.ts +189 -0
- package/spec/helpers/reporter.ts +41 -0
- package/spec/helpers/testDatabase.ts +107 -0
- package/spec/helpers/testHelpers.ts +192 -0
- package/spec/integration-critical.spec.ts +857 -0
- package/spec/integration.spec.ts +301 -0
- package/spec/repositories.spec.ts +181 -0
- package/spec/support/jasmine.json +7 -0
- package/spec/utils/security.spec.ts +243 -0
- package/src/OAuthInternalContext.ts +46 -0
- package/src/OAuthPlugin.ts +251 -0
- package/src/OAuthPluginContext.ts +53 -0
- package/src/OAuthPluginOptions.ts +122 -0
- package/src/handlers/CallbackOAuth.ts +238 -0
- package/src/handlers/InitiateOAuth.ts +99 -0
- package/src/index.ts +62 -0
- package/src/providers/GitHubProvider.ts +90 -0
- package/src/providers/GoogleProvider.ts +91 -0
- package/src/providers/OAuthProvider.ts +77 -0
- package/src/providers/OAuthProviderBase.ts +98 -0
- package/src/providers/ProviderRegistry.ts +27 -0
- package/src/repos/OAuthConnectionRepo.ts +41 -0
- package/src/repos/OAuthSessionRepo.ts +30 -0
- package/src/repos/TTL_INDEX_NOTE.md +28 -0
- package/src/schemas/CallbackRequest.ts +64 -0
- package/src/schemas/InitiateRequest.ts +10 -0
- package/src/schemas/OAuthConnection.ts +12 -0
- package/src/schemas/OAuthSession.ts +9 -0
- package/src/utils/encryption-utils.ts +148 -0
- package/src/utils/error-utils.ts +139 -0
- package/src/utils/state-utils.ts +70 -0
- package/src/utils/token-response-utils.ts +49 -0
- package/src/utils/validation-utils.ts +120 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client Integration with response_type=json
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* - Using OAuth plugin with mobile apps and SPAs
|
|
6
|
+
* - response_type=json for JSON responses instead of redirects
|
|
7
|
+
* - Handling OAuth flow in API clients
|
|
8
|
+
* - Storing and using JWT tokens in mobile/SPA context
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { FlinkApp } from "@flink-app/flink";
|
|
12
|
+
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
|
|
13
|
+
import { oauthPlugin } from "@flink-app/oauth-plugin";
|
|
14
|
+
import { FlinkContext, FlinkRepo } from "@flink-app/flink";
|
|
15
|
+
|
|
16
|
+
interface User {
|
|
17
|
+
_id?: string;
|
|
18
|
+
email: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
avatarUrl?: string;
|
|
21
|
+
oauthProviders: Array<{
|
|
22
|
+
provider: "github" | "google";
|
|
23
|
+
providerId: string;
|
|
24
|
+
}>;
|
|
25
|
+
roles: string[];
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class UserRepo extends FlinkRepo<AppContext, User> {
|
|
31
|
+
async findByEmail(email: string) {
|
|
32
|
+
return this.getOne({ email });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface AppContext extends FlinkContext {
|
|
37
|
+
repos: {
|
|
38
|
+
userRepo: UserRepo;
|
|
39
|
+
};
|
|
40
|
+
plugins: {
|
|
41
|
+
jwtAuth: any;
|
|
42
|
+
oauth: any;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function start() {
|
|
47
|
+
const app = new FlinkApp<AppContext>({
|
|
48
|
+
name: "OAuth API Client Example",
|
|
49
|
+
port: 3336,
|
|
50
|
+
|
|
51
|
+
db: {
|
|
52
|
+
uri: process.env.MONGODB_URI || "mongodb://localhost:27017/oauth-api-client-example",
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
auth: jwtAuthPlugin({
|
|
56
|
+
secret: process.env.JWT_SECRET || "your-super-secret-jwt-key",
|
|
57
|
+
getUser: async (tokenData) => {
|
|
58
|
+
const user = await app.ctx.repos.userRepo.getById(tokenData.userId);
|
|
59
|
+
if (!user) throw new Error("User not found");
|
|
60
|
+
return {
|
|
61
|
+
id: user._id,
|
|
62
|
+
email: user.email,
|
|
63
|
+
roles: user.roles,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
rolePermissions: {
|
|
67
|
+
user: ["read:own", "write:own"],
|
|
68
|
+
},
|
|
69
|
+
expiresIn: "30d", // Longer expiration for mobile apps
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
plugins: [
|
|
73
|
+
oauthPlugin({
|
|
74
|
+
providers: {
|
|
75
|
+
github: {
|
|
76
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
77
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
78
|
+
callbackUrl: process.env.GITHUB_CALLBACK_URL || "http://localhost:3336/oauth/github/callback",
|
|
79
|
+
},
|
|
80
|
+
google: {
|
|
81
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
82
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
83
|
+
callbackUrl: process.env.GOOGLE_CALLBACK_URL || "http://localhost:3336/oauth/google/callback",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
storeTokens: false,
|
|
88
|
+
|
|
89
|
+
onAuthSuccess: async ({ profile, provider }, ctx) => {
|
|
90
|
+
console.log(`\nAPI Client OAuth Success: ${provider} - ${profile.email}`);
|
|
91
|
+
|
|
92
|
+
let user = await ctx.repos.userRepo.findByEmail(profile.email);
|
|
93
|
+
|
|
94
|
+
if (!user) {
|
|
95
|
+
user = await ctx.repos.userRepo.create({
|
|
96
|
+
email: profile.email,
|
|
97
|
+
name: profile.name,
|
|
98
|
+
avatarUrl: profile.avatarUrl,
|
|
99
|
+
oauthProviders: [{ provider, providerId: profile.id }],
|
|
100
|
+
roles: ["user"],
|
|
101
|
+
createdAt: new Date(),
|
|
102
|
+
updatedAt: new Date(),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id, email: user.email }, user.roles);
|
|
107
|
+
|
|
108
|
+
console.log(`JWT token generated for API client`);
|
|
109
|
+
console.log(`Token will be returned as JSON (not redirect)`);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
user: {
|
|
113
|
+
_id: user._id,
|
|
114
|
+
email: user.email,
|
|
115
|
+
name: user.name,
|
|
116
|
+
avatarUrl: user.avatarUrl,
|
|
117
|
+
roles: user.roles,
|
|
118
|
+
},
|
|
119
|
+
token,
|
|
120
|
+
// redirectUrl is ignored when response_type=json
|
|
121
|
+
redirectUrl: "/dashboard",
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
onAuthError: async ({ error, provider }) => {
|
|
126
|
+
console.error(`OAuth error for ${provider}:`, error);
|
|
127
|
+
return {
|
|
128
|
+
redirectUrl: `/login?error=${error.code}`,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
|
|
134
|
+
// IMPORTANT: Configure CORS for API clients
|
|
135
|
+
cors: {
|
|
136
|
+
origin: [
|
|
137
|
+
"http://localhost:3000", // React/Vue dev server
|
|
138
|
+
"http://localhost:19006", // Expo dev server
|
|
139
|
+
"capacitor://localhost", // Capacitor iOS/Android
|
|
140
|
+
"ionic://localhost", // Ionic
|
|
141
|
+
/\.myapp\.com$/, // Production domains
|
|
142
|
+
],
|
|
143
|
+
credentials: true,
|
|
144
|
+
allowedHeaders: ["Authorization", "Content-Type"],
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await app.start();
|
|
149
|
+
|
|
150
|
+
console.log("\n===========================================");
|
|
151
|
+
console.log("OAuth API Client Integration Example");
|
|
152
|
+
console.log("===========================================");
|
|
153
|
+
console.log(`Server: http://localhost:3336`);
|
|
154
|
+
console.log(`\nKey Feature: response_type=json`);
|
|
155
|
+
console.log(` Add ?response_type=json to callback URL for JSON response`);
|
|
156
|
+
console.log(` Perfect for mobile apps, SPAs, and API clients`);
|
|
157
|
+
console.log(`\nExample URLs:`);
|
|
158
|
+
console.log(` Initiate: http://localhost:3336/oauth/github/initiate`);
|
|
159
|
+
console.log(` Callback: http://localhost:3336/oauth/github/callback?code=xxx&state=yyy&response_type=json`);
|
|
160
|
+
console.log("===========================================\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
start().catch((error) => {
|
|
164
|
+
console.error("Failed to start application:", error);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* CLIENT INTEGRATION EXAMPLES
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Example 1: React SPA with Popup Window
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
const ReactSPAExample = `
|
|
177
|
+
import React, { useState } from 'react';
|
|
178
|
+
|
|
179
|
+
function OAuthLogin() {
|
|
180
|
+
const [loading, setLoading] = useState(false);
|
|
181
|
+
|
|
182
|
+
const handleGitHubLogin = async () => {
|
|
183
|
+
setLoading(true);
|
|
184
|
+
|
|
185
|
+
// Open OAuth flow in popup window
|
|
186
|
+
const width = 600;
|
|
187
|
+
const height = 700;
|
|
188
|
+
const left = window.screen.width / 2 - width / 2;
|
|
189
|
+
const top = window.screen.height / 2 - height / 2;
|
|
190
|
+
|
|
191
|
+
const popup = window.open(
|
|
192
|
+
'http://localhost:3336/oauth/github/initiate',
|
|
193
|
+
'oauth_popup',
|
|
194
|
+
\`width=\${width},height=\${height},left=\${left},top=\${top}\`
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Listen for message from popup
|
|
198
|
+
window.addEventListener('message', async (event) => {
|
|
199
|
+
if (event.origin !== 'http://localhost:3336') return;
|
|
200
|
+
|
|
201
|
+
const { code, state } = event.data;
|
|
202
|
+
|
|
203
|
+
// Exchange code for token using response_type=json
|
|
204
|
+
const response = await fetch(
|
|
205
|
+
\`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`,
|
|
206
|
+
{
|
|
207
|
+
credentials: 'include'
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const data = await response.json();
|
|
212
|
+
|
|
213
|
+
if (data.token) {
|
|
214
|
+
// Store JWT token
|
|
215
|
+
localStorage.setItem('jwt_token', data.token);
|
|
216
|
+
|
|
217
|
+
// Store user data
|
|
218
|
+
localStorage.setItem('user', JSON.stringify(data.user));
|
|
219
|
+
|
|
220
|
+
// Close popup
|
|
221
|
+
popup?.close();
|
|
222
|
+
|
|
223
|
+
// Redirect to dashboard
|
|
224
|
+
window.location.href = '/dashboard';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setLoading(false);
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div>
|
|
233
|
+
<button onClick={handleGitHubLogin} disabled={loading}>
|
|
234
|
+
{loading ? 'Logging in...' : 'Login with GitHub'}
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Example 2: React Native with expo-auth-session
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
const ReactNativeExample = `
|
|
246
|
+
import React from 'react';
|
|
247
|
+
import { Button, View, Text } from 'react-native';
|
|
248
|
+
import * as WebBrowser from 'expo-web-browser';
|
|
249
|
+
import * as AuthSession from 'expo-auth-session';
|
|
250
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
251
|
+
|
|
252
|
+
// Tell expo-auth-session to finish the session
|
|
253
|
+
WebBrowser.maybeCompleteAuthSession();
|
|
254
|
+
|
|
255
|
+
function OAuthLogin() {
|
|
256
|
+
const [user, setUser] = React.useState(null);
|
|
257
|
+
|
|
258
|
+
// Configure OAuth discovery
|
|
259
|
+
const discovery = {
|
|
260
|
+
authorizationEndpoint: 'http://localhost:3336/oauth/github/initiate',
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Create auth request
|
|
264
|
+
const [request, response, promptAsync] = AuthSession.useAuthRequest(
|
|
265
|
+
{
|
|
266
|
+
clientId: 'not-used-for-our-flow',
|
|
267
|
+
redirectUri: AuthSession.makeRedirectUri({
|
|
268
|
+
native: 'myapp://oauth/callback',
|
|
269
|
+
}),
|
|
270
|
+
},
|
|
271
|
+
discovery
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
React.useEffect(() => {
|
|
275
|
+
if (response?.type === 'success') {
|
|
276
|
+
const { code, state } = response.params;
|
|
277
|
+
|
|
278
|
+
// Exchange code for token with response_type=json
|
|
279
|
+
fetch(
|
|
280
|
+
\`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`
|
|
281
|
+
)
|
|
282
|
+
.then(res => res.json())
|
|
283
|
+
.then(async (data) => {
|
|
284
|
+
// Store JWT token securely
|
|
285
|
+
await AsyncStorage.setItem('jwt_token', data.token);
|
|
286
|
+
|
|
287
|
+
// Store user data
|
|
288
|
+
await AsyncStorage.setItem('user', JSON.stringify(data.user));
|
|
289
|
+
|
|
290
|
+
setUser(data.user);
|
|
291
|
+
})
|
|
292
|
+
.catch(console.error);
|
|
293
|
+
}
|
|
294
|
+
}, [response]);
|
|
295
|
+
|
|
296
|
+
const handleLogin = () => {
|
|
297
|
+
promptAsync();
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (user) {
|
|
301
|
+
return (
|
|
302
|
+
<View>
|
|
303
|
+
<Text>Welcome, {user.name}!</Text>
|
|
304
|
+
<Text>Email: {user.email}</Text>
|
|
305
|
+
</View>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<View>
|
|
311
|
+
<Button
|
|
312
|
+
title="Login with GitHub"
|
|
313
|
+
onPress={handleLogin}
|
|
314
|
+
disabled={!request}
|
|
315
|
+
/>
|
|
316
|
+
</View>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Example 3: Mobile App with In-App Browser
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
const MobileInAppBrowserExample = `
|
|
326
|
+
import { InAppBrowser } from 'react-native-inappbrowser-reborn';
|
|
327
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
328
|
+
|
|
329
|
+
async function loginWithGitHub() {
|
|
330
|
+
try {
|
|
331
|
+
// Check if InAppBrowser is available
|
|
332
|
+
if (await InAppBrowser.isAvailable()) {
|
|
333
|
+
// Open OAuth URL in in-app browser
|
|
334
|
+
const result = await InAppBrowser.openAuth(
|
|
335
|
+
'http://localhost:3336/oauth/github/initiate',
|
|
336
|
+
'myapp://oauth/callback',
|
|
337
|
+
{
|
|
338
|
+
// iOS options
|
|
339
|
+
ephemeralWebSession: false,
|
|
340
|
+
// Android options
|
|
341
|
+
showTitle: false,
|
|
342
|
+
enableUrlBarHiding: true,
|
|
343
|
+
enableDefaultShare: false,
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (result.type === 'success') {
|
|
348
|
+
const url = new URL(result.url);
|
|
349
|
+
const code = url.searchParams.get('code');
|
|
350
|
+
const state = url.searchParams.get('state');
|
|
351
|
+
|
|
352
|
+
// Exchange code for token with response_type=json
|
|
353
|
+
const response = await fetch(
|
|
354
|
+
\`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const data = await response.json();
|
|
358
|
+
|
|
359
|
+
// Store JWT token securely
|
|
360
|
+
await AsyncStorage.setItem('jwt_token', data.token);
|
|
361
|
+
await AsyncStorage.setItem('user', JSON.stringify(data.user));
|
|
362
|
+
|
|
363
|
+
return data;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.error('OAuth error:', error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Usage in component
|
|
372
|
+
const LoginScreen = () => {
|
|
373
|
+
const handleLogin = async () => {
|
|
374
|
+
const result = await loginWithGitHub();
|
|
375
|
+
if (result) {
|
|
376
|
+
// Navigate to home screen
|
|
377
|
+
navigation.navigate('Home');
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return <Button title="Login with GitHub" onPress={handleLogin} />;
|
|
382
|
+
};
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Example 4: Vue.js SPA
|
|
387
|
+
*/
|
|
388
|
+
|
|
389
|
+
const VueJSExample = `
|
|
390
|
+
<template>
|
|
391
|
+
<div>
|
|
392
|
+
<button @click="loginWithGitHub" :disabled="loading">
|
|
393
|
+
{{ loading ? 'Logging in...' : 'Login with GitHub' }}
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
</template>
|
|
397
|
+
|
|
398
|
+
<script>
|
|
399
|
+
export default {
|
|
400
|
+
data() {
|
|
401
|
+
return {
|
|
402
|
+
loading: false,
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
methods: {
|
|
406
|
+
async loginWithGitHub() {
|
|
407
|
+
this.loading = true;
|
|
408
|
+
|
|
409
|
+
// Open OAuth in popup
|
|
410
|
+
const popup = window.open(
|
|
411
|
+
'http://localhost:3336/oauth/github/initiate',
|
|
412
|
+
'oauth_popup',
|
|
413
|
+
'width=600,height=700'
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Poll for popup to close or message
|
|
417
|
+
const checkPopup = setInterval(async () => {
|
|
418
|
+
try {
|
|
419
|
+
if (popup.closed) {
|
|
420
|
+
clearInterval(checkPopup);
|
|
421
|
+
this.loading = false;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check if popup has navigated to callback URL
|
|
426
|
+
const popupUrl = popup.location.href;
|
|
427
|
+
if (popupUrl.includes('/oauth/github/callback')) {
|
|
428
|
+
const url = new URL(popupUrl);
|
|
429
|
+
const code = url.searchParams.get('code');
|
|
430
|
+
const state = url.searchParams.get('state');
|
|
431
|
+
|
|
432
|
+
if (code && state) {
|
|
433
|
+
// Exchange for token with response_type=json
|
|
434
|
+
const response = await fetch(
|
|
435
|
+
\`http://localhost:3336/oauth/github/callback?code=\${code}&state=\${state}&response_type=json\`,
|
|
436
|
+
{ credentials: 'include' }
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const data = await response.json();
|
|
440
|
+
|
|
441
|
+
// Store JWT token
|
|
442
|
+
localStorage.setItem('jwt_token', data.token);
|
|
443
|
+
localStorage.setItem('user', JSON.stringify(data.user));
|
|
444
|
+
|
|
445
|
+
// Close popup
|
|
446
|
+
popup.close();
|
|
447
|
+
clearInterval(checkPopup);
|
|
448
|
+
|
|
449
|
+
// Redirect
|
|
450
|
+
this.$router.push('/dashboard');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch (e) {
|
|
454
|
+
// Cross-origin error - popup hasn't navigated yet
|
|
455
|
+
}
|
|
456
|
+
}, 500);
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
</script>
|
|
461
|
+
`;
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Example 5: Using JWT Token for API Requests
|
|
465
|
+
*/
|
|
466
|
+
|
|
467
|
+
const UsingJWTTokenExample = `
|
|
468
|
+
// After OAuth login, use JWT token for authenticated requests
|
|
469
|
+
|
|
470
|
+
// Get stored token
|
|
471
|
+
const token = localStorage.getItem('jwt_token');
|
|
472
|
+
// or for React Native
|
|
473
|
+
const token = await AsyncStorage.getItem('jwt_token');
|
|
474
|
+
|
|
475
|
+
// Make authenticated request
|
|
476
|
+
const response = await fetch('http://localhost:3336/api/profile', {
|
|
477
|
+
headers: {
|
|
478
|
+
'Authorization': \`Bearer \${token}\`,
|
|
479
|
+
'Content-Type': 'application/json'
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const userData = await response.json();
|
|
484
|
+
|
|
485
|
+
// Token refresh logic (if needed)
|
|
486
|
+
if (response.status === 401) {
|
|
487
|
+
// Token expired - redirect to login
|
|
488
|
+
localStorage.removeItem('jwt_token');
|
|
489
|
+
window.location.href = '/login';
|
|
490
|
+
}
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* IMPORTANT NOTES
|
|
495
|
+
*
|
|
496
|
+
* Response Type Formats:
|
|
497
|
+
*
|
|
498
|
+
* 1. response_type=json (for API clients):
|
|
499
|
+
* GET /oauth/github/callback?code=xxx&state=yyy&response_type=json
|
|
500
|
+
*
|
|
501
|
+
* Response:
|
|
502
|
+
* {
|
|
503
|
+
* "user": { "_id": "...", "email": "...", ... },
|
|
504
|
+
* "token": "eyJhbGc..."
|
|
505
|
+
* }
|
|
506
|
+
*
|
|
507
|
+
* 2. Default (redirect with token in query):
|
|
508
|
+
* GET /oauth/github/callback?code=xxx&state=yyy
|
|
509
|
+
*
|
|
510
|
+
* Redirects to:
|
|
511
|
+
* /dashboard?token=eyJhbGc...
|
|
512
|
+
*
|
|
513
|
+
* 3. URL fragment (if configured):
|
|
514
|
+
* Redirects to:
|
|
515
|
+
* /dashboard#token=eyJhbGc...
|
|
516
|
+
*
|
|
517
|
+
* Best Practices for API Clients:
|
|
518
|
+
*
|
|
519
|
+
* 1. Always use response_type=json for mobile apps and SPAs
|
|
520
|
+
* 2. Store JWT tokens in secure storage:
|
|
521
|
+
* - Mobile: AsyncStorage, Keychain, SecureStore
|
|
522
|
+
* - Web: localStorage (or sessionStorage for extra security)
|
|
523
|
+
* 3. Use HTTPS in production
|
|
524
|
+
* 4. Implement token refresh mechanism
|
|
525
|
+
* 5. Clear tokens on logout
|
|
526
|
+
* 6. Validate tokens on app startup
|
|
527
|
+
* 7. Handle token expiration gracefully
|
|
528
|
+
*
|
|
529
|
+
* Security Considerations:
|
|
530
|
+
*
|
|
531
|
+
* 1. NEVER store tokens in URL for long-term
|
|
532
|
+
* 2. Use HTTPS for all OAuth callbacks in production
|
|
533
|
+
* 3. Implement CORS properly for API clients
|
|
534
|
+
* 4. Validate redirect URIs on server
|
|
535
|
+
* 5. Use state parameter for CSRF protection
|
|
536
|
+
* 6. Implement rate limiting on OAuth endpoints
|
|
537
|
+
* 7. Log all OAuth attempts for security monitoring
|
|
538
|
+
*/
|
|
539
|
+
|
|
540
|
+
console.log("\n=== Client Integration Code Examples ===\n");
|
|
541
|
+
console.log("1. React SPA with Popup:");
|
|
542
|
+
console.log(ReactSPAExample);
|
|
543
|
+
console.log("\n2. React Native with expo-auth-session:");
|
|
544
|
+
console.log(ReactNativeExample);
|
|
545
|
+
console.log("\n3. Mobile In-App Browser:");
|
|
546
|
+
console.log(MobileInAppBrowserExample);
|
|
547
|
+
console.log("\n4. Vue.js SPA:");
|
|
548
|
+
console.log(VueJSExample);
|
|
549
|
+
console.log("\n5. Using JWT Token:");
|
|
550
|
+
console.log(UsingJWTTokenExample);
|