feat(auth): base available login methods off GoTrue providers (#1869)

* feat(auth): base login fields on GoTrue providers

Signed-off-by: Goostaf <gasplund2@gmail.com>

# Conflicts:
#	app/(auth)/login/login-client.tsx
#	app/(auth)/register/page.tsx

* fix: address feedback

Signed-off-by: Goostaf <gasplund2@gmail.com>

* chore: remove hardcoded Google enabled checks

Signed-off-by: Goostaf <gasplund2@gmail.com>

# Conflicts:
#	.env.example

* feat: show label when password login is disabled

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: use MicrosoftMark, correct comment

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: display custom providers

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: show when no methods are available

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: display custom provider labels

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: add SAML login path

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: show when no methods are available

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix: display SAML button when enabled

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix: preserve nextPath and broken key

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix: redirect test to client

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix: expose registerEnabled

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: provider allowlist and request timeout

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix: restore compact labels

Signed-off-by: Goostaf <gasplund2@gmail.com>

* refactor: move withTimeout implementation to utils

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix: include nextPath

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix: export function and test case

Signed-off-by: Goostaf <gasplund2@gmail.com>

* feat: only show SAML button if vars configured

Signed-off-by: Goostaf <gasplund2@gmail.com>

* fix(auth): map SAML sign-in error through getErrorMessage

The antipattern ratchet (check:guards, raw-user-error) rejects a raw
error.message reaching a user-visible sink. Route the signInWithSSO
error through getErrorMessage like the other auth error paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj
Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>

* feat(auth): GitHub brand mark on the provider button; decision log

GitHub allows its invertocat in solid black/white, so currentColor is
correct; custom OIDC providers keep the generic key icon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj
Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>

---------

Signed-off-by: Goostaf <gasplund2@gmail.com>
Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Goostaf
2026-08-31 09:13:39 +01:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent dc07ca8872
commit 6ac9679fb5
19 changed files with 1728 additions and 1078 deletions
@@ -4,31 +4,39 @@ import { useState } from 'react'
import { useLocale, useTranslations } from 'next-intl'
import { createClient } from '@/lib/supabase/client'
import { Button } from '@/components/ui/button'
import { Loader2 } from 'lucide-react'
import { Loader2, KeyRound } from 'lucide-react'
import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message'
import { GoogleMark } from '@/components/ui/provider-marks'
import { GitHubMark, GoogleMark, MicrosoftMark } from '@/components/ui/provider-marks'
import type { ResolvedProvider } from '@/lib/auth/gotrue-providers'
/**
* "Continue with Google" for the login and register pages.
*
* Kicks off the Supabase OAuth redirect; the round-trip lands in
* /auth/callback (PKCE code exchange), which owns MFA routing, invite
* acceptance and silent-team creation for OAuth sign-ins and sign-ups alike.
* The flow=oauth marker lets the callback tag failures so the login page
* shows Google-specific copy instead of the email-confirmation framing.
* Render the brand mark for a known provider, or a generic key icon for
* custom OIDC providers.
*/
export function GoogleAuthButton({
function ProviderMark({ provider }: { provider: ResolvedProvider }) {
if (provider.id === 'google') return <GoogleMark />
else if (provider.id === 'azure') return <MicrosoftMark />
else if (provider.id === 'github') return <GitHubMark />
else return <KeyRound className="h-4 w-4 text-muted-foreground" />
}
/**
* Generic OAuth login button. Works with any Supabase GoTrue provider.
*
* For known providers (Google, GitHub, etc.) the button shows the brand
* name; for custom OIDC providers it shows "Sign in with SSO" style text.
*
* Kicks off the Supabase OAuth redirect, with flow=oauth so
* /auth/callback can tag failures.
*/
export function OAuthButton({
provider,
onError,
compact = false,
next,
}: {
provider: ResolvedProvider
onError: (message: string) => void
/**
* Half-width alternative-method chip on the login panel: shows just the
* mark and "Google" (a brand name, never translated), with the full label
* kept as the accessible name.
*/
compact?: boolean
/**
* Post-auth destination, already passed through safeReturnTo by the caller.
@@ -51,7 +59,7 @@ export function GoogleAuthButton({
callback.searchParams.set('flow', 'oauth')
if (next && next !== '/') callback.searchParams.set('next', next)
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
provider: provider.id as Parameters<typeof supabase.auth.signInWithOAuth>[0]['provider'],
options: {
redirectTo: callback.toString(),
},
@@ -60,13 +68,14 @@ export function GoogleAuthButton({
onError(getErrorMessage(error, { context: 'auth', locale: errorLocale }))
setIsRedirecting(false)
}
// On success the browser navigates away; keep the spinner until then.
} catch (error) {
onError(getErrorMessage(error, { context: 'auth', locale: errorLocale }))
setIsRedirecting(false)
}
}
const label = tAuth('continue_with_provider', { provider: provider.label })
return (
<Button
type="button"
@@ -74,16 +83,16 @@ export function GoogleAuthButton({
className={compact ? 'h-10 w-full gap-2' : 'w-full h-11'}
onClick={handleClick}
disabled={isRedirecting}
aria-label={tAuth('continue_with_google')}
aria-label={label}
>
{isRedirecting ? (
<Loader2 className={compact ? 'h-4 w-4 animate-spin' : 'mr-2 h-4 w-4 animate-spin'} />
) : (
<span className={compact ? 'flex items-center' : 'mr-2 flex items-center'}>
<GoogleMark />
<ProviderMark provider={provider} />
</span>
)}
{compact ? 'Google' : tAuth('continue_with_google')}
{compact ? provider.label : label}
</Button>
)
}