signinwith 1.0.2 → 1.0.4
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 +24 -15
- package/package.json +4 -5
- package/react.jsx +196 -37
- package/readme.md +168 -106
- package/redirect-oauth.html +45 -0
- package/styles.css +14 -3
package/index.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import { OAuth2Client } from 'google-auth-library';
|
|
2
|
-
import appleSigninAuth from 'apple-signin-auth';
|
|
3
|
-
|
|
4
1
|
export const verifySigninGoogle = async (config, verificationData) => {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
audience: config.clientId,
|
|
9
|
-
});
|
|
10
|
-
const payload = ticket.getPayload();
|
|
2
|
+
const res = await fetch(`https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=${verificationData.credential}`);
|
|
3
|
+
const payload = await res.json();
|
|
4
|
+
if (payload.aud !== config.clientId) return { success: false, error: 'Invalid aud' };
|
|
11
5
|
return payload.email ? { success: true, email: payload.email } : { success: false, error: 'Email not found' };
|
|
12
6
|
};
|
|
13
7
|
|
|
@@ -19,18 +13,33 @@ export const verifySigninMeta = async (config, verificationData) => {
|
|
|
19
13
|
|
|
20
14
|
export const verifySigninApple = async (config, verificationData) => {
|
|
21
15
|
const { id_token } = verificationData;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
const form = new FormData();
|
|
17
|
+
form.append('id_token', id_token);
|
|
18
|
+
form.append('client_id', config.clientId);
|
|
19
|
+
const res = await fetch('https://appleid.apple.com/auth/verify', {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: form,
|
|
25
22
|
});
|
|
23
|
+
const result = await res.json();
|
|
24
|
+
if (!result.success) return { success: false, error: result.error || 'Invalid Apple signin' };
|
|
26
25
|
return result.email ? { success: true, email: result.email } : { success: false, error: 'Email not available from Apple' };
|
|
27
26
|
};
|
|
27
|
+
export const verifySigninDiscord = async (config, verificationData) => {
|
|
28
|
+
const res = await fetch('https://discord.com/api/v10/users/@me', {
|
|
29
|
+
headers: {
|
|
30
|
+
authorization: `Bearer ${verificationData.accessToken}`,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const profile = await res.json();
|
|
34
|
+
return profile.email ? { success: true, email: profile.email } : { success: false, error: 'Email not available from Discord' };
|
|
35
|
+
};
|
|
28
36
|
|
|
29
37
|
export default verifySignin = async (services, service, verificationData) => {
|
|
30
38
|
try {
|
|
31
|
-
if (service === 'google') return await verifySigninGoogle(services.google, verificationData);
|
|
32
|
-
if (service === 'meta') return await verifySigninMeta(services.meta, verificationData);
|
|
33
|
-
if (service === 'apple') return await verifySigninApple(services.apple, verificationData);
|
|
39
|
+
if (services.google && service === 'google') return await verifySigninGoogle(services.google, verificationData);
|
|
40
|
+
if (services.meta && service === 'meta') return await verifySigninMeta(services.meta, verificationData);
|
|
41
|
+
if (services.apple && service === 'apple') return await verifySigninApple(services.apple, verificationData);
|
|
42
|
+
if (services.discord && service === 'discord') return await verifySigninDiscord(services.discord, verificationData);
|
|
34
43
|
return { success: false, error: 'Unsupported service' };
|
|
35
44
|
} catch (err) {
|
|
36
45
|
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, Meta, Apple...",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Simple and straightforward library for sign in / sign up with thirdparty oAuth services like Google, Meta, Apple, Discord...",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
},
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|
|
11
11
|
"./react": "./react.jsx",
|
|
12
|
-
"./styles.css": "./styles.css"
|
|
12
|
+
"./styles.css": "./styles.css",
|
|
13
|
+
"./redirect-oauth.html": "./redirect-oauth.html"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"signinwith",
|
|
@@ -28,8 +29,6 @@
|
|
|
28
29
|
"author": "ybouane",
|
|
29
30
|
"license": "ISC",
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"apple-signin-auth": "^2.0.0",
|
|
32
|
-
"google-auth-library": "^9.15.1",
|
|
33
32
|
"react": "^19.1.0"
|
|
34
33
|
}
|
|
35
34
|
}
|
package/react.jsx
CHANGED
|
@@ -12,7 +12,12 @@ const AppleIcon = () => (
|
|
|
12
12
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000"><path fill="#FFFFFF" d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg>
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
// Subcomponent:
|
|
15
|
+
// Subcomponent: Discord Icon
|
|
16
|
+
const DiscordIcon = () => (
|
|
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
|
+
);
|
|
19
|
+
|
|
20
|
+
// Subcomponent: Facebook
|
|
16
21
|
export function SignInWithFacebook({ service, onSignin, onError }) {
|
|
17
22
|
useEffect(() => {
|
|
18
23
|
if (!window.FB) {
|
|
@@ -41,20 +46,46 @@ export function SignInWithFacebook({ service, onSignin, onError }) {
|
|
|
41
46
|
window.FB.login(function (response) {
|
|
42
47
|
if (response.authResponse) {
|
|
43
48
|
onSignin('facebook', { accessToken: response.authResponse.accessToken });
|
|
49
|
+
} else {
|
|
50
|
+
onError?.('Failed to log in with Facebook.');
|
|
44
51
|
}
|
|
45
52
|
}, { scope: 'email,public_profile' });
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
return <button className="signinwith-button signinwith-button-facebook" onClick={handleLogin}><FacebookIcon />Continue with Facebook</button>;
|
|
49
56
|
}
|
|
57
|
+
|
|
50
58
|
// Subcomponent: Google
|
|
51
59
|
export function SignInWithGoogle({ service, onSignin, onError }) {
|
|
52
60
|
useEffect(() => {
|
|
53
|
-
const
|
|
54
|
-
script
|
|
55
|
-
|
|
56
|
-
script
|
|
57
|
-
|
|
61
|
+
const scriptId = 'google-gsi-script';
|
|
62
|
+
let script = document.getElementById(scriptId);
|
|
63
|
+
|
|
64
|
+
if (!script) {
|
|
65
|
+
script = document.createElement('script');
|
|
66
|
+
script.id = scriptId;
|
|
67
|
+
script.src = 'https://accounts.google.com/gsi/client';
|
|
68
|
+
script.async = true;
|
|
69
|
+
script.defer = true;
|
|
70
|
+
script.onload = () => {
|
|
71
|
+
if (window.google?.accounts?.id) {
|
|
72
|
+
window.google.accounts.id.initialize({
|
|
73
|
+
client_id: service.clientId,
|
|
74
|
+
use_fedcm_for_prompt: true,
|
|
75
|
+
callback: (res) => {
|
|
76
|
+
onSignin('google', { credential: res.credential });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
onError?.('Google Sign-In script loaded but initialization failed.');
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
script.onerror = () => {
|
|
84
|
+
onError?.('Failed to load Google Sign-In script.');
|
|
85
|
+
};
|
|
86
|
+
document.body.appendChild(script);
|
|
87
|
+
} else if (window.google?.accounts?.id) {
|
|
88
|
+
// If script already exists, ensure it's initialized (might be needed if component remounts)
|
|
58
89
|
window.google.accounts.id.initialize({
|
|
59
90
|
client_id: service.clientId,
|
|
60
91
|
use_fedcm_for_prompt: true,
|
|
@@ -62,23 +93,33 @@ export function SignInWithGoogle({ service, onSignin, onError }) {
|
|
|
62
93
|
onSignin('google', { credential: res.credential });
|
|
63
94
|
}
|
|
64
95
|
});
|
|
65
|
-
}
|
|
66
|
-
document.body.appendChild(script);
|
|
96
|
+
}
|
|
67
97
|
|
|
98
|
+
|
|
99
|
+
// Cleanup function does not remove the script to allow multiple instances
|
|
100
|
+
// or remounts without reloading. Google's script handles this internally.
|
|
68
101
|
return () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
document.body.removeChild(scriptTag);
|
|
72
|
-
}
|
|
102
|
+
// Optional: You might want to hide the prompt if the component unmounts
|
|
103
|
+
// window.google?.accounts?.id?.cancel();
|
|
73
104
|
};
|
|
74
|
-
}, [service.clientId]);
|
|
105
|
+
}, [service.clientId, onSignin, onError]);
|
|
75
106
|
|
|
76
107
|
const handleGoogleLogin = (e) => {
|
|
77
108
|
e.stopPropagation();
|
|
78
109
|
e.preventDefault();
|
|
79
|
-
window.google
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
if (window.google?.accounts?.id) {
|
|
111
|
+
window.google.accounts.id.prompt((notification) => {
|
|
112
|
+
// Handle prompt UI notifications (e.g., closed, error)
|
|
113
|
+
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
|
114
|
+
// Potentially trigger a backup UX or just log
|
|
115
|
+
onError?.(notification.getNotDisplayedReason() || notification.getSkippedReason() || 'Google Sign-In prompt was not displayed or was skipped.');
|
|
116
|
+
} else if (notification.isDismissedMoment()) {
|
|
117
|
+
onError?.(notification.getDismissedReason() || 'Google Sign-In prompt was dismissed.');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
onError?.('Google Sign-In is not initialized.');
|
|
122
|
+
}
|
|
82
123
|
};
|
|
83
124
|
|
|
84
125
|
return (
|
|
@@ -91,38 +132,156 @@ export function SignInWithGoogle({ service, onSignin, onError }) {
|
|
|
91
132
|
// Subcomponent: Apple
|
|
92
133
|
export function SignInWithApple({ service, onSignin, onError }) {
|
|
93
134
|
useEffect(() => {
|
|
94
|
-
const
|
|
95
|
-
script
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
const scriptId = 'apple-auth-script';
|
|
136
|
+
let script = document.getElementById(scriptId);
|
|
137
|
+
|
|
138
|
+
const handleAppleSignInSuccess = (event) => {
|
|
139
|
+
onSignin('apple', event.detail.authorization);
|
|
140
|
+
};
|
|
141
|
+
const handleAppleSignInFailure = (event) => {
|
|
142
|
+
onError?.(event.detail.error || 'Sign in with Apple failed.');
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (!script) {
|
|
146
|
+
script = document.createElement('script');
|
|
147
|
+
script.id = scriptId;
|
|
148
|
+
script.src = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js';
|
|
149
|
+
script.async = true;
|
|
150
|
+
script.defer = true;
|
|
151
|
+
script.onload = () => {
|
|
152
|
+
try {
|
|
153
|
+
window.AppleID.auth.init({
|
|
154
|
+
clientId: service.clientId,
|
|
155
|
+
scope: service.scope || 'email name', // Default scope
|
|
156
|
+
redirectURI: service.redirectUri, // Default redirect URI
|
|
157
|
+
usePopup: true,
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error("Apple Sign In initialization failed:", error);
|
|
161
|
+
onError?.('Failed to initialize Apple Sign In.');
|
|
162
|
+
return; // Stop if init fails
|
|
163
|
+
}
|
|
164
|
+
// Add event listeners after successful initialization
|
|
165
|
+
document.addEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
|
|
166
|
+
document.addEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
|
|
167
|
+
};
|
|
168
|
+
script.onerror = () => {
|
|
169
|
+
onError?.('Failed to load Apple Sign In script.');
|
|
170
|
+
};
|
|
171
|
+
document.body.appendChild(script);
|
|
172
|
+
} else {
|
|
173
|
+
// If script exists, re-add listeners in case component remounted
|
|
174
|
+
document.removeEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
|
|
175
|
+
document.removeEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
|
|
176
|
+
document.addEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
|
|
177
|
+
document.addEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
// Cleanup listeners when component unmounts
|
|
182
|
+
document.removeEventListener('AppleIDSignInOnSuccess', handleAppleSignInSuccess);
|
|
183
|
+
document.removeEventListener('AppleIDSignInOnFailure', handleAppleSignInFailure);
|
|
106
184
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const handleAppleLogin = (e) => {
|
|
185
|
+
}, [service.clientId, service.scope, service.redirectUri, service.usePopup, onSignin, onError]);
|
|
186
|
+
|
|
187
|
+
const handleAppleLogin = async (e) => {
|
|
110
188
|
e.stopPropagation();
|
|
111
189
|
e.preventDefault();
|
|
112
|
-
|
|
190
|
+
try {
|
|
191
|
+
if (window.AppleID?.auth) {
|
|
192
|
+
await window.AppleID.auth.signIn();
|
|
193
|
+
} else {
|
|
194
|
+
onError?.('Apple Sign In is not initialized.');
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// This catch might handle errors from the signIn() call itself,
|
|
198
|
+
// though the event listener is the primary mechanism.
|
|
199
|
+
console.error("Apple Sign In error:", error);
|
|
200
|
+
onError?.('An error occurred during Apple Sign In.');
|
|
201
|
+
}
|
|
113
202
|
};
|
|
114
203
|
return <button className="signinwith-button signinwith-button-apple" onClick={handleAppleLogin}><AppleIcon />Continue with Apple</button>;
|
|
115
204
|
}
|
|
116
205
|
|
|
206
|
+
// Subcomponent: Discord
|
|
207
|
+
export function SignInWithDiscord({ service, onSignin, onError }) {
|
|
208
|
+
const handleDiscordLogin = (e) => {
|
|
209
|
+
e.stopPropagation();
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
|
|
212
|
+
const { clientId, redirectUri, scope = 'identify email' } = service; // Default scopes
|
|
213
|
+
|
|
214
|
+
if (!clientId || !redirectUri) {
|
|
215
|
+
console.error("Discord service configuration missing clientId or redirectUri.");
|
|
216
|
+
onError?.("Discord configuration is incomplete.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const params = new URLSearchParams({
|
|
222
|
+
client_id: clientId,
|
|
223
|
+
redirect_uri: redirectUri,
|
|
224
|
+
response_type: 'code',
|
|
225
|
+
scope: scope,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const discordAuthUrl = `https://discord.com/api/oauth2/authorize?${params.toString()}`;
|
|
229
|
+
|
|
230
|
+
window.open(discordAuthUrl);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error("Failed to initiate Discord login:", error);
|
|
233
|
+
onError?.("Failed to initiate Discord login.");
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const handleMessage = (event) => {
|
|
238
|
+
if (event.origin !== window.location.origin) {
|
|
239
|
+
return; // Only accept messages from the same origin
|
|
240
|
+
}
|
|
241
|
+
if (event.data && event.data.type === 'discordAuth') {
|
|
242
|
+
if (event.data.code) {
|
|
243
|
+
onSignin('discord', { code: event.data.code });
|
|
244
|
+
} else if (event.data.error) {
|
|
245
|
+
onError?.(`Discord login error: ${event.data.error}`);
|
|
246
|
+
} else {
|
|
247
|
+
onError?.('Unknown Discord login error.');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
window.addEventListener('message', handleMessage);
|
|
252
|
+
return () => {
|
|
253
|
+
window.removeEventListener('message', handleMessage);
|
|
254
|
+
};
|
|
255
|
+
}, [onSignin, onError]);
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<button className="signinwith-button signinwith-button-discord" onClick={handleDiscordLogin}>
|
|
259
|
+
<DiscordIcon />Continue with Discord
|
|
260
|
+
</button>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
117
265
|
// Main SignInWith Component
|
|
118
266
|
export default function SignInWith({ onSignin, onError, services, theme = 'light' }) {
|
|
119
267
|
return (
|
|
120
268
|
<div className={`signinwith-container signinwith-theme-${theme}`}>
|
|
121
|
-
{Object.entries(services).map(([key, config]) => {
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
269
|
+
{Object.entries(services || {}).map(([key, config]) => { // Added default {} for services
|
|
270
|
+
if (!config) return null; // Skip if config is null/undefined
|
|
271
|
+
|
|
272
|
+
switch (key.toLowerCase()) { // Use lowercase key for case-insensitivity
|
|
273
|
+
case 'google':
|
|
274
|
+
return <SignInWithGoogle key={key} service={config} onSignin={onSignin} onError={onError} />;
|
|
275
|
+
case 'facebook':
|
|
276
|
+
return <SignInWithFacebook key={key} service={config} onSignin={onSignin} onError={onError} />;
|
|
277
|
+
case 'apple':
|
|
278
|
+
return <SignInWithApple key={key} service={config} onSignin={onSignin} onError={onError} />;
|
|
279
|
+
case 'discord':
|
|
280
|
+
return <SignInWithDiscord key={key} service={config} onSignin={onSignin} onError={onError} />;
|
|
281
|
+
default:
|
|
282
|
+
console.warn(`Unsupported service key: ${key}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
126
285
|
})}
|
|
127
286
|
</div>
|
|
128
287
|
);
|
package/readme.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Sign In With
|
|
2
2
|
|
|
3
|
-
A simple and straightforward library for adding "Sign in with..." buttons (Google, Facebook/Meta, Apple) to your web application, handling both the frontend UI (React) and backend verification.
|
|
3
|
+
A simple and straightforward library for adding "Sign in with..." buttons (Google, Facebook/Meta, Apple, Discord) to your web application, handling both the frontend UI (React) and backend verification.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
* Easy integration for Google, Facebook (Meta), and
|
|
7
|
+
* Easy integration for Google, Facebook (Meta), Apple, and Discord 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.
|
|
@@ -27,47 +27,51 @@ import SignInWith from 'signinwith/react';
|
|
|
27
27
|
import 'signinwith/styles.css'; // Import the styles
|
|
28
28
|
|
|
29
29
|
function App() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
30
|
+
// Configuration for the services you want to enable
|
|
31
|
+
const services = {
|
|
32
|
+
google: {
|
|
33
|
+
clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
|
|
34
|
+
},
|
|
35
|
+
meta: {
|
|
36
|
+
appId: 'YOUR_FACEBOOK_APP_ID',
|
|
37
|
+
},
|
|
38
|
+
apple: {
|
|
39
|
+
clientId: 'YOUR_APPLE_SERVICE_ID', // e.g., com.mywebsite.signin
|
|
40
|
+
redirectUri: '/redirect-oauth.html'
|
|
41
|
+
// Ensure this callback route exists or handle the popup flow appropriately.
|
|
42
|
+
},
|
|
43
|
+
discord: {
|
|
44
|
+
clientId: 'YOUR_DISCORD_CLIENT_ID',
|
|
45
|
+
redirectUri: '/redirect-oauth.html',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Callback function when sign-in is successful on the frontend
|
|
50
|
+
const handleSignin = (service, data) => {
|
|
51
|
+
console.log(`Signed in with ${service}:`, data);
|
|
52
|
+
// Send 'service' and 'data' to your backend for verification
|
|
53
|
+
fetch('/api/auth/verify', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ service, data }),
|
|
57
|
+
})
|
|
58
|
+
.then(res => res.json())
|
|
59
|
+
.then(result => {
|
|
60
|
+
if (result.success) {
|
|
61
|
+
console.log('Backend verification successful:', result.email);
|
|
62
|
+
// Proceed with user session, redirect, etc.
|
|
63
|
+
} else {
|
|
64
|
+
console.error('Backend verification failed:', result.error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="signinwith-container">
|
|
71
|
+
<h2>Sign In</h2>
|
|
72
|
+
<SignInWith services={services} onSignin={handleSignin} onError={error=>console.error(error)} />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export default App;
|
|
@@ -75,8 +79,8 @@ export default App;
|
|
|
75
79
|
|
|
76
80
|
### Props for `SignInWith`
|
|
77
81
|
|
|
78
|
-
* `services` (Object, required): An object where keys are the service names (`google`, `meta`, `apple`) and values are their respective configuration objects.
|
|
79
|
-
* `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, `authorization` object for Apple) to be sent to your backend for verification.
|
|
82
|
+
* `services` (Object, required): An object where keys are the service names (`google`, `meta`, `apple`, `discord`) and values are their respective configuration objects.
|
|
83
|
+
* `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.
|
|
80
84
|
* `onError` (Function, optional): A callback function that receives an error string if there's an issue during the sign-in process.
|
|
81
85
|
|
|
82
86
|
## Backend Verification
|
|
@@ -94,44 +98,49 @@ app.use(express.json());
|
|
|
94
98
|
// Your service configurations (should match frontend, plus any secrets)
|
|
95
99
|
// Store these securely, e.g., in environment variables
|
|
96
100
|
const servicesConfig = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
google: {
|
|
102
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
103
|
+
},
|
|
104
|
+
facebook: {
|
|
105
|
+
// Facebook verification only needs the access token from the frontend
|
|
106
|
+
// No specific backend config needed here for the library function itself
|
|
107
|
+
// but you might need App ID/Secret for other Graph API calls.
|
|
108
|
+
},
|
|
109
|
+
apple: {
|
|
110
|
+
clientId: process.env.APPLE_CLIENT_ID, // Your Service ID (e.g., com.mywebsite.signin)
|
|
111
|
+
// The library uses apple-signin-auth which might require more config
|
|
112
|
+
// depending on how you generated your keys. Refer to its documentation.
|
|
113
|
+
},
|
|
114
|
+
discord: {
|
|
115
|
+
clientId: process.env.DISCORD_CLIENT_ID,
|
|
116
|
+
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
117
|
+
redirectUri: process.env.DISCORD_REDIRECT_URI,
|
|
118
|
+
},
|
|
110
119
|
};
|
|
111
120
|
|
|
112
121
|
app.post('/api/auth/verify', async (req, res) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
const { service, data } = req.body;
|
|
123
|
+
|
|
124
|
+
if (!service || !data) {
|
|
125
|
+
return res.status(400).json({ success: false, error: 'Missing service or data' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = await verifySignin(servicesConfig, service, data);
|
|
130
|
+
if (result.success) {
|
|
131
|
+
// Verification successful!
|
|
132
|
+
// Find or create user with result.email
|
|
133
|
+
console.log(`Verified ${service} sign in for: ${result.email}`);
|
|
134
|
+
res.json({ success: true, email: result.email });
|
|
135
|
+
} else {
|
|
136
|
+
// Verification failed
|
|
137
|
+
console.error(`Failed to verify ${service}: ${result.error}`);
|
|
138
|
+
res.status(401).json({ success: false, error: result.error || 'Verification failed' });
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`Error during ${service} verification:`, error);
|
|
142
|
+
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
143
|
+
}
|
|
135
144
|
});
|
|
136
145
|
|
|
137
146
|
const PORT = process.env.PORT || 3001;
|
|
@@ -139,19 +148,16 @@ app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
|
|
139
148
|
|
|
140
149
|
```
|
|
141
150
|
|
|
142
|
-
### Verification
|
|
151
|
+
### Verification
|
|
143
152
|
|
|
144
153
|
The main `verifySignin` function delegates to service-specific functions:
|
|
145
154
|
|
|
146
|
-
* `
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
* `
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
* `verifySigninApple(config, verificationData)`: Verifies Apple ID token.
|
|
153
|
-
* `config`: Needs `{ clientId }` (Your Apple Service ID).
|
|
154
|
-
* `verificationData`: Needs `{ id_token }` (from the `authorization` object).
|
|
155
|
+
* `verifyGoogleToken(servicesConfig, data)`: Verifies the Google ID token against Google's servers. It checks the token's signature, expiration, and audience (client ID).
|
|
156
|
+
* `verifyMetaToken(servicesConfig, data)`: Verifies the Meta (Facebook) access token by calling the Facebook Graph API. It checks if the token is valid and associated with your Facebook App.
|
|
157
|
+
* `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
|
+
* `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.
|
|
159
|
+
|
|
160
|
+
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.
|
|
155
161
|
|
|
156
162
|
## Styling
|
|
157
163
|
|
|
@@ -161,28 +167,84 @@ The buttons have the base class `signinwith-button` and provider-specific classe
|
|
|
161
167
|
* `signinwith-button-google` (Note: Google button uses `renderButton`, styling might be limited)
|
|
162
168
|
* `signinwith-button-meta`
|
|
163
169
|
* `signinwith-button-apple`
|
|
170
|
+
* `signinwith-button-discord`
|
|
164
171
|
|
|
165
172
|
You can override these styles in your own CSS. The container in the example uses `signinwith-container` for layout.
|
|
166
173
|
|
|
167
174
|
## Configuration Details
|
|
168
175
|
|
|
169
176
|
* **Google:**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
* Create a project in [Google Cloud Console](https://console.cloud.google.com/).
|
|
178
|
+
* Set up OAuth 2.0 Credentials (Web application type).
|
|
179
|
+
* Add your domain(s) to "Authorized JavaScript origins".
|
|
180
|
+
* Add your backend callback URL (if applicable) to "Authorized redirect URIs".
|
|
181
|
+
* Get your **Client ID**.
|
|
175
182
|
* **Facebook (Meta):**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
* Create an App at [Facebook for Developers](https://developers.facebook.com/).
|
|
184
|
+
* Set up "Facebook Login for Business".
|
|
185
|
+
* Add your domain(s) to the App Domains and Site URL in the app settings.
|
|
186
|
+
* Get your **App ID**.
|
|
180
187
|
* **Apple:**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
* Register an App ID and a Service ID in your [Apple Developer Account](https://developer.apple.com/).
|
|
189
|
+
* Configure "Sign in with Apple" for your App ID.
|
|
190
|
+
* Associate your domain(s) with the Service ID and verify them.
|
|
191
|
+
* Get your **Service ID** (used as `clientId`). You might also need a private key for certain backend operations, but `apple-signin-auth` handles basic token verification with just the `clientId` (audience check). Ensure the frontend `redirectUri` is correctly handled (either via a backend route or popup flow).
|
|
192
|
+
* **Discord:**
|
|
193
|
+
* Create an App at [Discord Developer Portal](https://discord.com/developers/applications).
|
|
194
|
+
* Set up OAuth2.
|
|
195
|
+
* Add your redirect URI.
|
|
196
|
+
* Get your **Client ID** and **Client Secret**. Store the client secret securely on the backend.
|
|
197
|
+
|
|
198
|
+
## Redirect URI (Popup)
|
|
199
|
+
|
|
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:
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<!DOCTYPE html>
|
|
204
|
+
<html>
|
|
205
|
+
<head>
|
|
206
|
+
<title>Discord Authentication</title>
|
|
207
|
+
<style>
|
|
208
|
+
body {
|
|
209
|
+
font-family: sans-serif;
|
|
210
|
+
display: flex;
|
|
211
|
+
justify-content: center;
|
|
212
|
+
align-items: center;
|
|
213
|
+
height: 100vh;
|
|
214
|
+
margin: 0;
|
|
215
|
+
background-color: #f0f0f0;
|
|
216
|
+
}
|
|
217
|
+
.container {
|
|
218
|
+
text-align: center;
|
|
219
|
+
padding: 20px;
|
|
220
|
+
background-color: #fff;
|
|
221
|
+
border-radius: 8px;
|
|
222
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
225
|
+
</head>
|
|
226
|
+
<body>
|
|
227
|
+
<div class="container">
|
|
228
|
+
<p>You can close this window now.</p>
|
|
229
|
+
<script>
|
|
230
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
231
|
+
const code = urlParams.get('code');
|
|
232
|
+
const error = urlParams.get('error');
|
|
233
|
+
|
|
234
|
+
if (code) {
|
|
235
|
+
window.opener.postMessage({ type: 'discordAuth', code: code }, window.location.origin);
|
|
236
|
+
} else if(error) {
|
|
237
|
+
window.opener.postMessage({ type: 'oauthError', error: error }, window.location.origin);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Attempt to close the window
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
window.close();
|
|
243
|
+
}, 0);
|
|
244
|
+
</script>
|
|
245
|
+
</div>
|
|
246
|
+
</body>
|
|
247
|
+
</html>
|
|
248
|
+
```
|
|
187
249
|
|
|
188
|
-
|
|
250
|
+
This HTML file extracts the authorization code (or error) from the URL hash and sends it back to the main window using `postMessage`. 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.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Discord Authentication</title>
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
font-family: sans-serif;
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
align-items: center;
|
|
11
|
+
height: 100vh;
|
|
12
|
+
margin: 0;
|
|
13
|
+
background-color: #f0f0f0;
|
|
14
|
+
}
|
|
15
|
+
.container {
|
|
16
|
+
text-align: center;
|
|
17
|
+
padding: 20px;
|
|
18
|
+
background-color: #fff;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<div class="container">
|
|
26
|
+
<p>You can close this window now.</p>
|
|
27
|
+
<script>
|
|
28
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
29
|
+
const code = urlParams.get('code');
|
|
30
|
+
const error = urlParams.get('error');
|
|
31
|
+
|
|
32
|
+
if (code) {
|
|
33
|
+
window.opener.postMessage({ type: 'discordAuth', code: code }, window.location.origin);
|
|
34
|
+
} else if(error) {
|
|
35
|
+
window.opener.postMessage({ type: 'oauthError', error: error }, window.location.origin);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Attempt to close the window
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
window.close();
|
|
41
|
+
}, 0);
|
|
42
|
+
</script>
|
|
43
|
+
</div>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
package/styles.css
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
text-decoration: none;
|
|
19
19
|
align-self: stretch;
|
|
20
20
|
box-sizing: border-box;
|
|
21
|
-
transition:
|
|
21
|
+
transition: opacity 0.3s ease;
|
|
22
22
|
color: #3c4043;
|
|
23
23
|
background-color: #ffffff;
|
|
24
24
|
|
|
@@ -30,15 +30,26 @@
|
|
|
30
30
|
|
|
31
31
|
&:hover {
|
|
32
32
|
background-color: #f8f9fa;
|
|
33
|
+
opacity:0.9;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
&.signinwith-button-facebook {
|
|
36
37
|
background-color: #1877F2;
|
|
37
38
|
color: white;
|
|
38
|
-
border-color: #
|
|
39
|
+
border-color: #1877F2;
|
|
39
40
|
|
|
40
41
|
&:hover {
|
|
41
|
-
background-color: #
|
|
42
|
+
background-color: #1877F2;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&.signinwith-button-discord {
|
|
47
|
+
background-color: #5d67f7;
|
|
48
|
+
color: white;
|
|
49
|
+
border-color: #5d67f7;
|
|
50
|
+
|
|
51
|
+
&:hover {
|
|
52
|
+
background-color: #5d67f7;
|
|
42
53
|
}
|
|
43
54
|
}
|
|
44
55
|
|