signinwith 1.0.12 → 1.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.
- package/index.js +56 -0
- package/package.json +2 -2
- package/react.jsx +161 -19
- package/readme.md +59 -14
- package/redirect-oauth.html +42 -16
- package/styles.css +12 -2
package/index.js
CHANGED
|
@@ -52,12 +52,68 @@ export const verifySigninDiscord = async (config, verificationData) => {
|
|
|
52
52
|
return profile.email ? { success: true, email: profile.email } : { success: false, error: 'Email not available from Discord' };
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
export const verifySigninGithub = async (config, verificationData) => {
|
|
56
|
+
if (!verificationData?.code) return { success: false, error: 'Missing GitHub authorization code' };
|
|
57
|
+
if (!config?.clientId || !config?.clientSecret) return { success: false, error: 'GitHub clientId and clientSecret are required' };
|
|
58
|
+
|
|
59
|
+
const params = new URLSearchParams();
|
|
60
|
+
params.append('client_id', config.clientId);
|
|
61
|
+
params.append('client_secret', config.clientSecret);
|
|
62
|
+
params.append('code', verificationData.code);
|
|
63
|
+
if (config.redirectUri) params.append('redirect_uri', config.redirectUri);
|
|
64
|
+
if (verificationData.codeVerifier) params.append('code_verifier', verificationData.codeVerifier);
|
|
65
|
+
|
|
66
|
+
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Accept': 'application/json',
|
|
70
|
+
},
|
|
71
|
+
body: params,
|
|
72
|
+
});
|
|
73
|
+
const token = await tokenResponse.json();
|
|
74
|
+
if (!tokenResponse.ok || token.error) {
|
|
75
|
+
return { success: false, error: token.error_description || 'Failed to exchange GitHub code for token' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const accessToken = token.access_token;
|
|
79
|
+
const apiHeaders = {
|
|
80
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
81
|
+
'Accept': 'application/vnd.github+json',
|
|
82
|
+
'User-Agent': config.userAgent || 'signinwith',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const profileRes = await fetch('https://api.github.com/user', { headers: apiHeaders });
|
|
86
|
+
const profile = await profileRes.json();
|
|
87
|
+
if (!profileRes.ok) {
|
|
88
|
+
return { success: false, error: profile.message || 'Failed to fetch GitHub profile' };
|
|
89
|
+
}
|
|
90
|
+
if (profile.email) {
|
|
91
|
+
return { success: true, email: profile.email };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const emailRes = await fetch('https://api.github.com/user/emails', { headers: apiHeaders });
|
|
95
|
+
if (emailRes.ok) {
|
|
96
|
+
const emails = await emailRes.json();
|
|
97
|
+
if (Array.isArray(emails)) {
|
|
98
|
+
const primary = emails.find((item) => item.primary && item.verified);
|
|
99
|
+
const verified = emails.find((item) => item.verified);
|
|
100
|
+
const email = primary?.email || verified?.email;
|
|
101
|
+
if (email) {
|
|
102
|
+
return { success: true, email };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { success: false, error: 'Email not available from GitHub' };
|
|
108
|
+
};
|
|
109
|
+
|
|
55
110
|
export default async function verifySignin (services, service, verificationData) {
|
|
56
111
|
try {
|
|
57
112
|
if (services.google && service === 'google') return await verifySigninGoogle(services.google, verificationData);
|
|
58
113
|
if (services.facebook && service === 'facebook') return await verifySigninFacebook(services.facebook, verificationData);
|
|
59
114
|
if (services.apple && service === 'apple') return await verifySigninApple(services.apple, verificationData);
|
|
60
115
|
if (services.discord && service === 'discord') return await verifySigninDiscord(services.discord, verificationData);
|
|
116
|
+
if (services.github && service === 'github') return await verifySigninGithub(services.github, verificationData);
|
|
61
117
|
return { success: false, error: 'Unsupported service' };
|
|
62
118
|
} catch (err) {
|
|
63
119
|
return { success: false, error: err.message || 'Unknown error' };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signinwith",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Simple and straightforward library for sign in / sign up with thirdparty oAuth services like Google,
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Simple and straightforward library for sign in / sign up with thirdparty oAuth services like Google, Facebook, Apple, Discord...",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
},
|
package/react.jsx
CHANGED
|
@@ -17,6 +17,17 @@ const DiscordIcon = () => (
|
|
|
17
17
|
<svg viewBox="0 -28.5 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M216.856 16.597A208.502 208.502 0 0 0 164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 0 0-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0 0 79.735 175.3a136.413 136.413 0 0 1-21.846-10.632 108.636 108.636 0 0 0 5.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 0 0 5.355 4.237 136.07 136.07 0 0 1-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36ZM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18Zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18Z" fill="#FFFFFF"/></svg>
|
|
18
18
|
);
|
|
19
19
|
|
|
20
|
+
const GithubIcon = () => (
|
|
21
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
22
|
+
<path
|
|
23
|
+
fill-rule="evenodd"
|
|
24
|
+
clip-rule="evenodd"
|
|
25
|
+
d="M12 0C5.37 0 0 5.42 0 12.108c0 5.354 3.438 9.892 8.205 11.496.6.115.82-.264.82-.585 0-.288-.01-1.049-.016-2.06-3.338.737-4.042-1.642-4.042-1.642-.547-1.412-1.336-1.788-1.336-1.788-1.09-.758.083-.743.083-.743 1.205.086 1.84 1.255 1.84 1.255 1.07 1.864 2.807 1.326 3.492 1.015.108-.793.42-1.326.763-1.63-2.665-.31-5.466-1.366-5.466-6.075 0-1.342.465-2.44 1.232-3.3-.124-.311-.535-1.565.118-3.263 0 0 1.007-.33 3.3 1.26a11.23 11.23 0 0 1 3.004-.41c1.02.004 2.047.14 3.004.41 2.29-1.59 3.296-1.26 3.296-1.26.655 1.698.244 2.952.12 3.263.77.86 1.23 1.958 1.23 3.3 0 4.724-2.807 5.76-5.48 6.064.43.379.823 1.126.823 2.27 0 1.64-.015 2.96-.015 3.363 0 .324.216.704.826.583C20.565 22 24 17.46 24 12.108 24 5.42 18.627 0 12 0Z"
|
|
26
|
+
fill="#FFFFFF"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
|
|
20
31
|
// Subcomponent: Facebook
|
|
21
32
|
export function SignInWithFacebook({ service, onSignin, onError }) {
|
|
22
33
|
useEffect(() => {
|
|
@@ -205,11 +216,54 @@ export function SignInWithApple({ service, onSignin, onError }) {
|
|
|
205
216
|
|
|
206
217
|
// Subcomponent: Discord
|
|
207
218
|
export function SignInWithDiscord({ service, onSignin, onError }) {
|
|
219
|
+
const popupRef = useRef(null);
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (typeof BroadcastChannel === 'undefined') {
|
|
223
|
+
console.warn('BroadcastChannel is not supported in this browser. Discord login will not work.');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const channel = new BroadcastChannel('signinwith-discord');
|
|
228
|
+
const handleChannelMessage = (event) => {
|
|
229
|
+
const data = event.data;
|
|
230
|
+
if (!data || data.service !== 'discord') return;
|
|
231
|
+
|
|
232
|
+
if (data.code) {
|
|
233
|
+
onSignin('discord', { code: data.code, state: data.state });
|
|
234
|
+
} else if (data.error) {
|
|
235
|
+
onError?.(`Discord login error: ${data.error}`);
|
|
236
|
+
} else {
|
|
237
|
+
onError?.('Unknown Discord login error.');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (popupRef.current && !popupRef.current.closed) {
|
|
241
|
+
popupRef.current.close();
|
|
242
|
+
}
|
|
243
|
+
window.__signinwithPendingProvider = null;
|
|
244
|
+
window.__signinwithPendingProviderState = null;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
channel.addEventListener('message', handleChannelMessage);
|
|
248
|
+
|
|
249
|
+
return () => {
|
|
250
|
+
channel.removeEventListener('message', handleChannelMessage);
|
|
251
|
+
channel.close();
|
|
252
|
+
};
|
|
253
|
+
}, [onSignin, onError]);
|
|
254
|
+
|
|
208
255
|
const handleDiscordLogin = (e) => {
|
|
209
256
|
e.stopPropagation();
|
|
210
257
|
e.preventDefault();
|
|
211
258
|
|
|
212
|
-
const {
|
|
259
|
+
const {
|
|
260
|
+
clientId,
|
|
261
|
+
redirectUri,
|
|
262
|
+
scope = 'identify email',
|
|
263
|
+
state = 'discord',
|
|
264
|
+
prompt = 'consent',
|
|
265
|
+
popupFeatures = 'width=500,height=700',
|
|
266
|
+
} = service; // Default scopes
|
|
213
267
|
|
|
214
268
|
if (!clientId || !redirectUri) {
|
|
215
269
|
console.error("Discord service configuration missing clientId or redirectUri.");
|
|
@@ -217,46 +271,132 @@ export function SignInWithDiscord({ service, onSignin, onError }) {
|
|
|
217
271
|
return;
|
|
218
272
|
}
|
|
219
273
|
|
|
274
|
+
if (typeof BroadcastChannel === 'undefined') {
|
|
275
|
+
onError?.('BroadcastChannel API is not available in this browser.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
220
279
|
try {
|
|
221
280
|
const params = new URLSearchParams({
|
|
222
281
|
client_id: clientId,
|
|
223
282
|
redirect_uri: redirectUri,
|
|
224
283
|
response_type: 'code',
|
|
225
284
|
scope: scope,
|
|
285
|
+
state,
|
|
226
286
|
});
|
|
287
|
+
if (prompt) params.append('prompt', prompt);
|
|
227
288
|
|
|
228
289
|
const discordAuthUrl = `https://discord.com/api/oauth2/authorize?${params.toString()}`;
|
|
229
|
-
|
|
230
|
-
window.
|
|
290
|
+
window.__signinwithPendingProvider = 'discord';
|
|
291
|
+
window.__signinwithPendingProviderState = state;
|
|
292
|
+
popupRef.current = window.open(discordAuthUrl, '_blank', popupFeatures);
|
|
231
293
|
} catch (error) {
|
|
232
294
|
console.error("Failed to initiate Discord login:", error);
|
|
233
295
|
onError?.("Failed to initiate Discord login.");
|
|
234
296
|
}
|
|
235
297
|
};
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<button className="signinwith-button signinwith-button-discord" onClick={handleDiscordLogin}>
|
|
301
|
+
<DiscordIcon />Continue with Discord
|
|
302
|
+
</button>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Subcomponent: GitHub
|
|
307
|
+
export function SignInWithGithub({ service, onSignin, onError }) {
|
|
308
|
+
const popupRef = useRef(null);
|
|
309
|
+
|
|
236
310
|
useEffect(() => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
311
|
+
if (typeof BroadcastChannel === 'undefined') {
|
|
312
|
+
console.warn('BroadcastChannel is not supported in this browser. GitHub login will not work.');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const channel = new BroadcastChannel('signinwith-github');
|
|
317
|
+
const handleChannelMessage = (event) => {
|
|
318
|
+
const data = event.data;
|
|
319
|
+
if (!data || data.service !== 'github') return;
|
|
320
|
+
|
|
321
|
+
if (data.code) {
|
|
322
|
+
onSignin('github', { code: data.code, state: data.state });
|
|
323
|
+
} else if (data.error) {
|
|
324
|
+
onError?.(`GitHub login error: ${data.error}`);
|
|
325
|
+
} else {
|
|
326
|
+
onError?.('Unknown GitHub login error.');
|
|
240
327
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
} else if (event.data.error) {
|
|
245
|
-
onError?.(`Discord login error: ${event.data.error}`);
|
|
246
|
-
} else {
|
|
247
|
-
onError?.('Unknown Discord login error.');
|
|
248
|
-
}
|
|
328
|
+
|
|
329
|
+
if (popupRef.current && !popupRef.current.closed) {
|
|
330
|
+
popupRef.current.close();
|
|
249
331
|
}
|
|
332
|
+
window.__signinwithPendingProvider = null;
|
|
333
|
+
window.__signinwithPendingProviderState = null;
|
|
250
334
|
};
|
|
251
|
-
|
|
335
|
+
|
|
336
|
+
channel.addEventListener('message', handleChannelMessage);
|
|
337
|
+
|
|
252
338
|
return () => {
|
|
253
|
-
|
|
339
|
+
channel.removeEventListener('message', handleChannelMessage);
|
|
340
|
+
channel.close();
|
|
254
341
|
};
|
|
255
342
|
}, [onSignin, onError]);
|
|
256
343
|
|
|
344
|
+
const handleGithubLogin = (e) => {
|
|
345
|
+
e.stopPropagation();
|
|
346
|
+
e.preventDefault();
|
|
347
|
+
|
|
348
|
+
const {
|
|
349
|
+
clientId,
|
|
350
|
+
redirectUri,
|
|
351
|
+
scope = 'read:user user:email',
|
|
352
|
+
state = 'github',
|
|
353
|
+
allowSignup = true,
|
|
354
|
+
popupFeatures = 'width=500,height=700',
|
|
355
|
+
codeChallenge,
|
|
356
|
+
codeChallengeMethod,
|
|
357
|
+
login,
|
|
358
|
+
} = service || {};
|
|
359
|
+
|
|
360
|
+
if (!clientId || !redirectUri) {
|
|
361
|
+
onError?.('GitHub configuration is incomplete.');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (typeof BroadcastChannel === 'undefined') {
|
|
366
|
+
onError?.('BroadcastChannel API is not available in this browser.');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const params = new URLSearchParams({
|
|
372
|
+
client_id: clientId,
|
|
373
|
+
redirect_uri: redirectUri,
|
|
374
|
+
response_type: 'code',
|
|
375
|
+
scope,
|
|
376
|
+
state,
|
|
377
|
+
allow_signup: allowSignup ? 'true' : 'false',
|
|
378
|
+
});
|
|
379
|
+
if (login) params.append('login', login);
|
|
380
|
+
if (codeChallenge) {
|
|
381
|
+
params.append('code_challenge', codeChallenge);
|
|
382
|
+
if (codeChallengeMethod) {
|
|
383
|
+
params.append('code_challenge_method', codeChallengeMethod);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const githubAuthUrl = `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
388
|
+
window.__signinwithPendingProvider = 'github';
|
|
389
|
+
window.__signinwithPendingProviderState = state;
|
|
390
|
+
popupRef.current = window.open(githubAuthUrl, '_blank', popupFeatures);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error("Failed to initiate GitHub login:", error);
|
|
393
|
+
onError?.("Failed to initiate GitHub login.");
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
257
397
|
return (
|
|
258
|
-
<button className="signinwith-button signinwith-button-
|
|
259
|
-
<
|
|
398
|
+
<button className="signinwith-button signinwith-button-github" onClick={handleGithubLogin}>
|
|
399
|
+
<GithubIcon />Continue with GitHub
|
|
260
400
|
</button>
|
|
261
401
|
);
|
|
262
402
|
}
|
|
@@ -278,6 +418,8 @@ export default function SignInWith({ onSignin, onError, services, theme = 'light
|
|
|
278
418
|
return <SignInWithApple key={key} service={config} onSignin={onSignin} onError={onError} />;
|
|
279
419
|
case 'discord':
|
|
280
420
|
return <SignInWithDiscord key={key} service={config} onSignin={onSignin} onError={onError} />;
|
|
421
|
+
case 'github':
|
|
422
|
+
return <SignInWithGithub key={key} service={config} onSignin={onSignin} onError={onError} />;
|
|
281
423
|
default:
|
|
282
424
|
console.warn(`Unsupported service key: ${key}`);
|
|
283
425
|
return null;
|
|
@@ -285,4 +427,4 @@ export default function SignInWith({ onSignin, onError, services, theme = 'light
|
|
|
285
427
|
})}
|
|
286
428
|
</div>
|
|
287
429
|
);
|
|
288
|
-
}
|
|
430
|
+
}
|
package/readme.md
CHANGED
|
@@ -4,7 +4,7 @@ A simple and straightforward library for adding "Sign in with..." buttons (Googl
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
* Easy integration for Google, Facebook (Meta), Apple, and
|
|
7
|
+
* Easy integration for Google, Facebook (Meta), Apple, Discord, and GitHub sign-in.
|
|
8
8
|
* React components for the frontend buttons.
|
|
9
9
|
* Backend utility functions to verify the identity tokens/access tokens.
|
|
10
10
|
* Basic customizable styling.
|
|
@@ -32,7 +32,7 @@ function App() {
|
|
|
32
32
|
google: {
|
|
33
33
|
clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
|
|
34
34
|
},
|
|
35
|
-
|
|
35
|
+
facebook: {
|
|
36
36
|
appId: 'YOUR_FACEBOOK_APP_ID',
|
|
37
37
|
},
|
|
38
38
|
apple: {
|
|
@@ -44,6 +44,11 @@ function App() {
|
|
|
44
44
|
clientId: 'YOUR_DISCORD_CLIENT_ID',
|
|
45
45
|
redirectUri: '/redirect-oauth.html',
|
|
46
46
|
},
|
|
47
|
+
github: {
|
|
48
|
+
clientId: 'YOUR_GITHUB_OAUTH_APP_CLIENT_ID',
|
|
49
|
+
redirectUri: '/redirect-oauth.html',
|
|
50
|
+
scope: 'read:user user:email',
|
|
51
|
+
},
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
// Callback function when sign-in is successful on the frontend
|
|
@@ -79,7 +84,7 @@ export default App;
|
|
|
79
84
|
|
|
80
85
|
### Props for `SignInWith`
|
|
81
86
|
|
|
82
|
-
* `services` (Object, required): An object where keys are the service names (`google`, `
|
|
87
|
+
* `services` (Object, required): An object where keys are the service names (`google`, `facebook`, `apple`, `discord`, `github`) and values are their respective configuration objects.
|
|
83
88
|
* `onSignin` (Function, required): A callback function that receives `(serviceName, data)` when a sign-in attempt is successful on the client-side. `data` contains the necessary information (e.g., `credential` for Google, `accessToken` for Facebook/Discord, `authorization` object for Apple) to be sent to your backend for verification.
|
|
84
89
|
* `onError` (Function, optional): A callback function that receives an error string if there's an issue during the sign-in process.
|
|
85
90
|
|
|
@@ -116,6 +121,12 @@ const servicesConfig = {
|
|
|
116
121
|
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
117
122
|
redirectUri: process.env.DISCORD_REDIRECT_URI,
|
|
118
123
|
},
|
|
124
|
+
github: {
|
|
125
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
126
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
127
|
+
redirectUri: process.env.GITHUB_REDIRECT_URI,
|
|
128
|
+
userAgent: 'your-app-name',
|
|
129
|
+
},
|
|
119
130
|
};
|
|
120
131
|
|
|
121
132
|
app.post('/api/auth/verify', async (req, res) => {
|
|
@@ -153,9 +164,10 @@ app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
|
|
153
164
|
The main `verifySignin` function delegates to service-specific functions:
|
|
154
165
|
|
|
155
166
|
* `verifyGoogleToken(servicesConfig, data)`: Verifies the Google ID token against Google's servers. It checks the token's signature, expiration, and audience (client ID).
|
|
156
|
-
* `
|
|
167
|
+
* `verifyFacebookToken(servicesConfig, data)`: Verifies the Facebook (Meta) access token by calling the Facebook Graph API. It checks if the token is valid and associated with your Facebook App.
|
|
157
168
|
* `verifyAppleToken(servicesConfig, data)`: Verifies the Apple authorization code. It checks the code's validity and audience (client ID). It may require additional configuration depending on your Apple Developer setup (e.g., private key).
|
|
158
169
|
* `verifyDiscordToken(servicesConfig, data)`: Exchanges the Discord authorization code for an access token, then uses the access token to fetch user information from the Discord API. It verifies that the code is valid and associated with your Discord application.
|
|
170
|
+
* `verifyGithubToken(servicesConfig, data)`: Exchanges the GitHub authorization code for an access token, then fetches the user's primary or any verified email via the GitHub API. Requires the OAuth app's `clientId`, `clientSecret`, and `redirectUri`.
|
|
159
171
|
|
|
160
172
|
Each of these functions returns a promise that resolves to an object with either a `success: true` and the user's `email`, or `success: false` and an `error` message.
|
|
161
173
|
|
|
@@ -165,7 +177,7 @@ Basic styles are provided in `signinwith/styles.css`. You can import this file d
|
|
|
165
177
|
|
|
166
178
|
The buttons have the base class `signinwith-button` and provider-specific classes:
|
|
167
179
|
* `signinwith-button-google` (Note: Google button uses `renderButton`, styling might be limited)
|
|
168
|
-
* `signinwith-button-
|
|
180
|
+
* `signinwith-button-facebook`
|
|
169
181
|
* `signinwith-button-apple`
|
|
170
182
|
* `signinwith-button-discord`
|
|
171
183
|
|
|
@@ -194,16 +206,21 @@ You can override these styles in your own CSS. The container in the example uses
|
|
|
194
206
|
* Set up OAuth2.
|
|
195
207
|
* Add your redirect URI.
|
|
196
208
|
* Get your **Client ID** and **Client Secret**. Store the client secret securely on the backend.
|
|
209
|
+
* **GitHub:**
|
|
210
|
+
* Create an OAuth App at [GitHub Developer Settings](https://github.com/settings/developers).
|
|
211
|
+
* Set the Authorization callback URL to your hosted `redirect-oauth.html` (or any page that forwards the OAuth code to your frontend).
|
|
212
|
+
* Copy the **Client ID** and **Client Secret**. Provide them to `verifySigninGithub` on the backend.
|
|
213
|
+
* Optional: customize scopes (`read:user user:email` by default) or `allow_signup`/`state` via the React component configuration.
|
|
197
214
|
|
|
198
215
|
## Redirect URI (Popup)
|
|
199
216
|
|
|
200
|
-
For Discord and Apple, the `redirectUri` in the frontend configuration should point to a static HTML file (e.g., `redirect-oauth.html`) that handles the OAuth2 code. This file facilitates communication between the popup window and the main application window. Place the following content to be the page that is set as the redirectUri:
|
|
217
|
+
For Discord, GitHub, and Apple, the `redirectUri` in the frontend configuration should point to a static HTML file (e.g., `redirect-oauth.html`) that handles the OAuth2 code. This file facilitates communication between the popup window and the main application window. Place the following content to be the page that is set as the redirectUri:
|
|
201
218
|
|
|
202
219
|
```html
|
|
203
220
|
<!DOCTYPE html>
|
|
204
221
|
<html>
|
|
205
222
|
<head>
|
|
206
|
-
<title>
|
|
223
|
+
<title>OAuth Authentication</title>
|
|
207
224
|
<style>
|
|
208
225
|
body {
|
|
209
226
|
font-family: sans-serif;
|
|
@@ -227,29 +244,57 @@ For Discord and Apple, the `redirectUri` in the frontend configuration should po
|
|
|
227
244
|
<div class="container">
|
|
228
245
|
<p>You can close this window now.</p>
|
|
229
246
|
<script>
|
|
247
|
+
(function handleOauthResult(){
|
|
230
248
|
const urlParams = new URLSearchParams(window.location.search);
|
|
231
249
|
const code = urlParams.get('code');
|
|
232
250
|
const error = urlParams.get('error');
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
const state = urlParams.get('state');
|
|
252
|
+
const serviceParam = urlParams.get('service') || urlParams.get('provider');
|
|
253
|
+
const defaultService = window.opener && window.opener.__signinwithPendingProvider ? window.opener.__signinwithPendingProvider : null;
|
|
254
|
+
const fallbackState = window.opener && window.opener.__signinwithPendingProviderState ? window.opener.__signinwithPendingProviderState : null;
|
|
255
|
+
let service = serviceParam || defaultService || (state || '').replace(/^.*:/, '') || 'discord';
|
|
256
|
+
if (!service) service = 'discord';
|
|
257
|
+
|
|
258
|
+
const message = {
|
|
259
|
+
type: `${service}Auth`,
|
|
260
|
+
service,
|
|
261
|
+
};
|
|
262
|
+
if (code) message.code = code;
|
|
263
|
+
if (state || fallbackState) message.state = state || fallbackState;
|
|
264
|
+
if (error) message.error = error;
|
|
265
|
+
|
|
266
|
+
const channelName = `signinwith-${service}`;
|
|
267
|
+
if (typeof BroadcastChannel !== 'undefined') {
|
|
268
|
+
try {
|
|
269
|
+
const channel = new BroadcastChannel(channelName);
|
|
270
|
+
channel.postMessage(message);
|
|
271
|
+
channel.close();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error('Failed to broadcast OAuth result', err);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
const target = window.opener || window.parent;
|
|
277
|
+
if (target && typeof target.postMessage === 'function') {
|
|
278
|
+
target.postMessage(message, window.location.origin);
|
|
279
|
+
}
|
|
238
280
|
}
|
|
239
281
|
|
|
240
282
|
// Attempt to close the window
|
|
241
283
|
setTimeout(() => {
|
|
242
284
|
window.close();
|
|
243
285
|
}, 0);
|
|
286
|
+
})();
|
|
244
287
|
</script>
|
|
245
288
|
</div>
|
|
246
289
|
</body>
|
|
247
290
|
</html>
|
|
248
291
|
```
|
|
249
292
|
|
|
250
|
-
This HTML file extracts the authorization code (or error) from the URL hash and sends it back to the main window using `postMessage
|
|
293
|
+
This HTML file extracts the authorization code (or error) from the URL hash and sends it back to the main window using `BroadcastChannel` (falling back to `postMessage` when unavailable). The main application needs to listen for this message and then proceed with the backend verification. This approach is necessary because Discord's OAuth flow, when initiated in a popup, requires a way to pass the authorization code back to the originating window.
|
|
294
|
+
|
|
295
|
+
The helper page uses the `BroadcastChannel` API to publish the OAuth result back to the main window and automatically infers the provider name (Discord, GitHub, etc.) based on the pending popup initiation, so you can re-use the same redirect page for every provider that relies on a popup + code flow.
|
|
251
296
|
|
|
252
297
|
Note: you can import this page as a string (to then return it as a response from your server) via:
|
|
253
298
|
```javascript
|
|
254
299
|
import htmlContent from 'signinwith/redirect-oauth.html?raw';
|
|
255
|
-
```
|
|
300
|
+
```
|
package/redirect-oauth.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
-
<title>
|
|
4
|
+
<title>OAuth Authentication</title>
|
|
5
5
|
<style>
|
|
6
6
|
body {
|
|
7
7
|
font-family: sans-serif;
|
|
@@ -24,22 +24,48 @@
|
|
|
24
24
|
<body>
|
|
25
25
|
<div class="container">
|
|
26
26
|
<p>You can close this window now.</p>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
<script>
|
|
28
|
+
(function handleOauthResult() {
|
|
29
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
30
|
+
const code = urlParams.get('code');
|
|
31
|
+
const error = urlParams.get('error');
|
|
32
|
+
const state = urlParams.get('state');
|
|
33
|
+
const serviceParam = urlParams.get('service') || urlParams.get('provider');
|
|
34
|
+
const defaultService = window.opener && window.opener.__signinwithPendingProvider ? window.opener.__signinwithPendingProvider : null;
|
|
35
|
+
const fallbackState = window.opener && window.opener.__signinwithPendingProviderState ? window.opener.__signinwithPendingProviderState : null;
|
|
36
|
+
let service = serviceParam || defaultService || (state || '').replace(/^.*:/, '') || 'discord';
|
|
37
|
+
if (!service) service = 'discord';
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
const message = {
|
|
40
|
+
type: `${service}Auth`,
|
|
41
|
+
service,
|
|
42
|
+
};
|
|
43
|
+
if (code) message.code = code;
|
|
44
|
+
if (state || fallbackState) message.state = state || fallbackState;
|
|
45
|
+
if (error) message.error = error;
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
const channelName = `signinwith-${service}`;
|
|
48
|
+
if (typeof BroadcastChannel !== 'undefined') {
|
|
49
|
+
try {
|
|
50
|
+
const channel = new BroadcastChannel(channelName);
|
|
51
|
+
channel.postMessage(message);
|
|
52
|
+
channel.close();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error('Failed to broadcast OAuth result', err);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const target = window.opener || window.parent;
|
|
58
|
+
if (target && typeof target.postMessage === 'function') {
|
|
59
|
+
target.postMessage(message, window.location.origin);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Attempt to close the window
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
window.close();
|
|
66
|
+
}, 0);
|
|
67
|
+
})();
|
|
68
|
+
</script>
|
|
43
69
|
</div>
|
|
44
70
|
</body>
|
|
45
|
-
</html>
|
|
71
|
+
</html>
|
package/styles.css
CHANGED
|
@@ -57,10 +57,20 @@
|
|
|
57
57
|
background-color: #000000;
|
|
58
58
|
color: white;
|
|
59
59
|
border-color: #000000;
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
&:hover {
|
|
62
62
|
background-color: #333333;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
&.signinwith-button-github {
|
|
67
|
+
background-color: #24292f;
|
|
68
|
+
color: white;
|
|
69
|
+
border-color: #24292f;
|
|
70
|
+
|
|
71
|
+
&:hover {
|
|
72
|
+
background-color: #1b1f23;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
65
75
|
}
|
|
66
|
-
}
|
|
76
|
+
}
|