Skip to content

Commit d4ceaf8

Browse files
authored
Merge pull request #138 from reqcore-inc/feat/authentication-methods
feat: implement social sign-in for Google, GitHub, and Microsoft with configuration support
2 parents ee34d86 + b94ffd9 commit d4ceaf8

13 files changed

Lines changed: 4872 additions & 14 deletions

File tree

.env.example

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,24 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000
9696

9797
# Display name for the SSO button (default: "SSO")
9898
# OIDC_PROVIDER_NAME=Company SSO
99+
100+
# ─── Optional: Social Sign-In (Google, GitHub, Microsoft) ────────────────────
101+
# Enable social login buttons on the sign-in and sign-up pages.
102+
# Each provider requires both CLIENT_ID and CLIENT_SECRET to be set.
103+
# When configured, "Continue with <Provider>" buttons appear on the auth pages.
104+
105+
# Google — Create credentials at https://console.cloud.google.com/apis/credentials
106+
# Redirect URI: https://yourdomain.com/api/auth/callback/google
107+
# AUTH_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
108+
# AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-google-client-secret
109+
110+
# GitHub — Create an OAuth App at https://github.com/settings/developers
111+
# Redirect URI: https://yourdomain.com/api/auth/callback/github
112+
# AUTH_GITHUB_CLIENT_ID=your-github-client-id
113+
# AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
114+
115+
# Microsoft — Register an app at https://portal.azure.com → App registrations
116+
# Redirect URI: https://yourdomain.com/api/auth/callback/microsoft
117+
# AUTH_MICROSOFT_CLIENT_ID=your-microsoft-client-id
118+
# AUTH_MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
119+
# AUTH_MICROSOFT_TENANT_ID=common

app/pages/auth/sign-in.vue

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,27 @@ const email = ref("");
1616
const password = ref("");
1717
const error = ref("");
1818
const isLoading = ref(false);
19+
const socialLoading = ref<string | null>(null);
1920
const ssoRedirecting = ref(false);
2021
const route = useRoute();
2122
const config = useRuntimeConfig();
2223
const localePath = useLocalePath();
2324
const { track } = useTrack();
24-
const oidcEnabled = computed(() => config.public.oidcEnabled as boolean);
25+
26+
const { data: authProviders } = await useFetch('/api/auth/providers');
27+
const oidcEnabled = computed(() => authProviders.value?.oidc ?? false);
2528
const oidcProviderName = computed(
26-
() => (config.public.oidcProviderName as string) || "SSO",
29+
() => authProviders.value?.oidcProviderName || "SSO",
2730
);
2831
32+
const socialProviders = computed(() => {
33+
const providers: { id: string; name: string }[] = [];
34+
if (authProviders.value?.google) providers.push({ id: "google", name: "Google" });
35+
if (authProviders.value?.github) providers.push({ id: "github", name: "GitHub" });
36+
if (authProviders.value?.microsoft) providers.push({ id: "microsoft", name: "Microsoft" });
37+
return providers;
38+
});
39+
2940
onMounted(() => track("signin_page_viewed"));
3041
3142
if (route.query.live === "1") {
@@ -163,6 +174,31 @@ async function handleEnterpriseSso() {
163174
ssoRedirecting.value = false;
164175
}
165176
}
177+
178+
/**
179+
* Social sign-in — Google, GitHub, Microsoft.
180+
* Uses better-auth's built-in signIn.social() which handles the full OAuth redirect flow.
181+
*/
182+
async function handleSocialSignIn(providerId: string) {
183+
socialLoading.value = providerId;
184+
error.value = "";
185+
const pendingInvitation = route.query.invitation as string | undefined;
186+
const callbackURL = pendingInvitation
187+
? localePath(`/auth/accept-invitation/${pendingInvitation}`)
188+
: localePath("/dashboard");
189+
try {
190+
await authClient.signIn.social({
191+
provider: providerId as "google" | "github" | "microsoft",
192+
callbackURL,
193+
});
194+
} catch (e: unknown) {
195+
error.value =
196+
e instanceof Error
197+
? e.message
198+
: "Social sign-in failed. Please try again.";
199+
socialLoading.value = null;
200+
}
201+
}
166202
</script>
167203

168204
<template>
@@ -180,12 +216,61 @@ async function handleEnterpriseSso() {
180216
{{ error }}
181217
</div>
182218

219+
<!-- Social sign-in providers (Google, GitHub, Microsoft) -->
220+
<template v-if="socialProviders.length">
221+
<div class="flex flex-col gap-2.5">
222+
<button
223+
v-for="provider in socialProviders"
224+
:key="provider.id"
225+
type="button"
226+
:disabled="!!socialLoading || isLoading || ssoRedirecting"
227+
class="relative px-4 py-2.5 rounded-lg text-sm font-medium shadow-sm transition-all flex items-center justify-center gap-3 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 text-surface-800 dark:text-surface-200 hover:bg-surface-50 dark:hover:bg-surface-700 hover:border-surface-300 dark:hover:border-surface-600"
228+
@click="handleSocialSignIn(provider.id)"
229+
>
230+
<template v-if="socialLoading === provider.id">
231+
<svg class="animate-spin size-4 text-surface-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
232+
Redirecting…
233+
</template>
234+
<template v-else>
235+
<!-- Google icon -->
236+
<svg v-if="provider.id === 'google'" class="size-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
237+
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
238+
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
239+
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
240+
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
241+
</svg>
242+
<!-- GitHub icon -->
243+
<svg v-else-if="provider.id === 'github'" class="size-5 text-surface-900 dark:text-surface-100" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
244+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
245+
</svg>
246+
<!-- Microsoft icon -->
247+
<svg v-else-if="provider.id === 'microsoft'" class="size-5" viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg">
248+
<rect x="1" y="1" width="10" height="10" fill="#F25022"/>
249+
<rect x="12" y="1" width="10" height="10" fill="#7FBA00"/>
250+
<rect x="1" y="12" width="10" height="10" fill="#00A4EF"/>
251+
<rect x="12" y="12" width="10" height="10" fill="#FFB900"/>
252+
</svg>
253+
Continue with {{ provider.name }}
254+
</template>
255+
</button>
256+
</div>
257+
258+
<div v-if="!oidcEnabled" class="relative">
259+
<div class="absolute inset-0 flex items-center">
260+
<div class="w-full border-t border-surface-200 dark:border-surface-700" />
261+
</div>
262+
<div class="relative flex justify-center text-xs">
263+
<span class="bg-white dark:bg-surface-900 px-2 text-surface-400">or continue with email</span>
264+
</div>
265+
</div>
266+
</template>
267+
183268
<!-- Self-hosted OIDC SSO — only shown when global OIDC is configured via environment variables -->
184269
<template v-if="oidcEnabled">
185270
<button
186271
type="button"
187272
:disabled="isLoading"
188-
class="px-4 py-2.5 bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-lg text-sm font-semibold shadow-md hover:bg-surface-800 dark:hover:bg-surface-100 disabled:opacity-60 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2.5 ring-1 ring-surface-700 dark:ring-surface-300"
273+
class="px-4 py-2.5 bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-lg text-sm font-semibold shadow-md cursor-pointer hover:bg-surface-800 dark:hover:bg-surface-100 disabled:opacity-60 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2.5 ring-1 ring-surface-700 dark:ring-surface-300"
189274
@click="handleSelfHostedSso"
190275
>
191276
<template v-if="isLoading">Redirecting…</template>
@@ -240,7 +325,7 @@ async function handleEnterpriseSso() {
240325
<button
241326
type="submit"
242327
:disabled="isLoading"
243-
class="mt-2 px-4 py-2.5 bg-brand-600 text-white rounded-md text-sm font-medium hover:bg-brand-700 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
328+
class="mt-2 px-4 py-2.5 bg-brand-600 text-white rounded-md text-sm font-medium cursor-pointer hover:bg-brand-700 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
244329
>
245330
{{ isLoading ? "Signing in…" : "Sign in" }}
246331
</button>
@@ -264,7 +349,7 @@ async function handleEnterpriseSso() {
264349
<button
265350
type="button"
266351
:disabled="ssoRedirecting"
267-
class="px-4 py-2.5 bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-lg text-sm font-semibold shadow-md hover:bg-surface-800 dark:hover:bg-surface-100 disabled:opacity-60 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2.5 ring-1 ring-surface-700 dark:ring-surface-300"
352+
class="px-4 py-2.5 bg-surface-900 dark:bg-white text-white dark:text-surface-900 rounded-lg text-sm font-semibold shadow-md cursor-pointer hover:bg-surface-800 dark:hover:bg-surface-100 disabled:opacity-60 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2.5 ring-1 ring-surface-700 dark:ring-surface-300"
268353
@click="handleEnterpriseSso"
269354
>
270355
<ShieldCheck class="size-4" />

app/pages/auth/sign-up.vue

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,20 @@ const error = ref("");
1919
const isLoading = ref(false);
2020
const localePath = useLocalePath();
2121
const { track } = useTrack();
22-
const config = useRuntimeConfig();
23-
const oidcEnabled = computed(() => config.public.oidcEnabled as boolean);
22+
const { data: authProviders } = await useFetch('/api/auth/providers');
23+
const oidcEnabled = computed(() => authProviders.value?.oidc ?? false);
2424
const oidcProviderName = computed(
25-
() => (config.public.oidcProviderName as string) || "SSO",
25+
() => authProviders.value?.oidcProviderName || "SSO",
2626
);
27+
const socialLoading = ref<string | null>(null);
28+
29+
const socialProviders = computed(() => {
30+
const providers: { id: string; name: string }[] = [];
31+
if (authProviders.value?.google) providers.push({ id: "google", name: "Google" });
32+
if (authProviders.value?.github) providers.push({ id: "github", name: "GitHub" });
33+
if (authProviders.value?.microsoft) providers.push({ id: "microsoft", name: "Microsoft" });
34+
return providers;
35+
});
2736
2837
onMounted(() => track("signup_page_viewed"));
2938
@@ -108,6 +117,31 @@ async function handleSsoSignUp() {
108117
isLoading.value = false;
109118
}
110119
}
120+
121+
/**
122+
* Social sign-up — Google, GitHub, Microsoft.
123+
* Uses better-auth's built-in signIn.social() which handles the full OAuth redirect flow.
124+
* New users are auto-registered on first social login.
125+
*/
126+
async function handleSocialSignUp(providerId: string) {
127+
socialLoading.value = providerId;
128+
error.value = "";
129+
const callbackURL = pendingInvitation.value
130+
? localePath(`/auth/accept-invitation/${pendingInvitation.value}`)
131+
: localePath("/onboarding/create-org");
132+
try {
133+
await authClient.signIn.social({
134+
provider: providerId as "google" | "github" | "microsoft",
135+
callbackURL,
136+
});
137+
} catch (e: unknown) {
138+
error.value =
139+
e instanceof Error
140+
? e.message
141+
: "Social sign-up failed. Please try again.";
142+
socialLoading.value = null;
143+
}
144+
}
111145
</script>
112146

113147
<template>
@@ -125,12 +159,61 @@ async function handleSsoSignUp() {
125159
{{ error }}
126160
</div>
127161

162+
<!-- Social sign-up providers (Google, GitHub, Microsoft) -->
163+
<template v-if="socialProviders.length">
164+
<div class="flex flex-col gap-2.5">
165+
<button
166+
v-for="provider in socialProviders"
167+
:key="provider.id"
168+
type="button"
169+
:disabled="!!socialLoading || isLoading"
170+
class="relative px-4 py-2.5 rounded-lg text-sm font-medium shadow-sm transition-all flex items-center justify-center gap-3 cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed border border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 text-surface-800 dark:text-surface-200 hover:bg-surface-50 dark:hover:bg-surface-700 hover:border-surface-300 dark:hover:border-surface-600"
171+
@click="handleSocialSignUp(provider.id)"
172+
>
173+
<template v-if="socialLoading === provider.id">
174+
<svg class="animate-spin size-4 text-surface-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" /></svg>
175+
Redirecting…
176+
</template>
177+
<template v-else>
178+
<!-- Google icon -->
179+
<svg v-if="provider.id === 'google'" class="size-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
180+
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
181+
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
182+
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
183+
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
184+
</svg>
185+
<!-- GitHub icon -->
186+
<svg v-else-if="provider.id === 'github'" class="size-5 text-surface-900 dark:text-surface-100" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
187+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
188+
</svg>
189+
<!-- Microsoft icon -->
190+
<svg v-else-if="provider.id === 'microsoft'" class="size-5" viewBox="0 0 23 23" xmlns="http://www.w3.org/2000/svg">
191+
<rect x="1" y="1" width="10" height="10" fill="#F25022"/>
192+
<rect x="12" y="1" width="10" height="10" fill="#7FBA00"/>
193+
<rect x="1" y="12" width="10" height="10" fill="#00A4EF"/>
194+
<rect x="12" y="12" width="10" height="10" fill="#FFB900"/>
195+
</svg>
196+
Continue with {{ provider.name }}
197+
</template>
198+
</button>
199+
</div>
200+
201+
<div v-if="!oidcEnabled" class="relative">
202+
<div class="absolute inset-0 flex items-center">
203+
<div class="w-full border-t border-surface-200 dark:border-surface-700" />
204+
</div>
205+
<div class="relative flex justify-center text-xs">
206+
<span class="bg-white dark:bg-surface-900 px-2 text-surface-400">or continue with email</span>
207+
</div>
208+
</div>
209+
</template>
210+
128211
<!-- SSO sign-up — only shown when OIDC is configured via environment variables -->
129212
<template v-if="oidcEnabled">
130213
<button
131214
type="button"
132215
:disabled="isLoading"
133-
class="px-4 py-2.5 bg-surface-800 dark:bg-surface-200 text-white dark:text-surface-900 rounded-md text-sm font-medium hover:bg-surface-900 dark:hover:bg-surface-300 disabled:opacity-60 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
216+
class="px-4 py-2.5 bg-surface-800 dark:bg-surface-200 text-white dark:text-surface-900 rounded-md text-sm font-medium cursor-pointer hover:bg-surface-900 dark:hover:bg-surface-300 disabled:opacity-60 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
134217
@click="handleSsoSignUp"
135218
>
136219
<template v-if="isLoading">Redirecting…</template>
@@ -211,7 +294,7 @@ async function handleSsoSignUp() {
211294
<button
212295
type="submit"
213296
:disabled="isLoading"
214-
class="mt-2 px-4 py-2.5 bg-brand-600 text-white rounded-md text-sm font-medium hover:bg-brand-700 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
297+
class="mt-2 px-4 py-2.5 bg-brand-600 text-white rounded-md text-sm font-medium cursor-pointer hover:bg-brand-700 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
215298
>
216299
{{ isLoading ? "Creating account…" : "Sign up" }}
217300
</button>

server/api/auth/providers.get.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* GET /api/auth/providers
3+
*
4+
* Returns which authentication providers are enabled at runtime.
5+
* This replaces build-time runtimeConfig flags so that Docker images
6+
* built without OAuth credentials still show the correct buttons when
7+
* credentials are injected at runtime (e.g. Railway, self-hosting).
8+
*/
9+
export default defineEventHandler(() => {
10+
return {
11+
google: !!(
12+
process.env.AUTH_GOOGLE_CLIENT_ID &&
13+
process.env.AUTH_GOOGLE_CLIENT_SECRET
14+
),
15+
github: !!(
16+
process.env.AUTH_GITHUB_CLIENT_ID &&
17+
process.env.AUTH_GITHUB_CLIENT_SECRET
18+
),
19+
microsoft: !!(
20+
process.env.AUTH_MICROSOFT_CLIENT_ID &&
21+
process.env.AUTH_MICROSOFT_CLIENT_SECRET
22+
),
23+
oidc: !!(
24+
process.env.OIDC_CLIENT_ID &&
25+
process.env.OIDC_CLIENT_SECRET &&
26+
process.env.OIDC_DISCOVERY_URL
27+
),
28+
oidcProviderName: process.env.OIDC_PROVIDER_NAME || "SSO",
29+
};
30+
});

0 commit comments

Comments
 (0)