forked from mirror/crumbs-original
feat(auth): OAuth authorize and callback endpoints
Dynamic [provider] routes for Google, GitHub, and OIDC. Stores state and PKCE verifier in httpOnly cookies. Callback validates code, finds/links user by provider ID or email (invite-only), and creates session. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Fred Cabassut
co-authored by
Claude Opus 4.6
parent
86c5d29e62
commit
67764cee31
@@ -266,6 +266,36 @@ export function updateUserRole(userId: number, role: 'admin' | 'user'): void {
|
||||
db.update(users).set({ role }).where(eq(users.id, userId)).run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by OAuth provider, or by email. Invite-only: rejects if no matching user.
|
||||
*/
|
||||
export function findOrLinkOAuthUser(
|
||||
provider: string,
|
||||
providerId: string,
|
||||
email: string,
|
||||
name: string
|
||||
): User | null {
|
||||
// First try exact provider+providerId match
|
||||
const byProvider = db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(and(eq(users.authProvider, provider), eq(users.providerId, providerId)))
|
||||
.get();
|
||||
if (byProvider) return toUser(byProvider);
|
||||
|
||||
// Try matching by email (invite-only: user must already exist)
|
||||
const byEmail = db.select().from(users).where(eq(users.email, email)).get();
|
||||
if (!byEmail) return null; // No matching user — not invited
|
||||
|
||||
// Link this OAuth provider to the existing user
|
||||
db.update(users)
|
||||
.set({ authProvider: provider, providerId, displayName: byEmail.displayName || name })
|
||||
.where(eq(users.id, byEmail.id))
|
||||
.run();
|
||||
|
||||
return toUser({ ...byEmail, authProvider: provider, providerId });
|
||||
}
|
||||
|
||||
export function updateUserProfile(
|
||||
userId: number,
|
||||
data: { displayName?: string; email?: string }
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { createGoogleAuthUrl } from '$lib/server/oauth/google.js';
|
||||
import { createGithubAuthUrl } from '$lib/server/oauth/github.js';
|
||||
import { createOidcAuthUrl } from '$lib/server/oauth/oidc.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
const { provider } = params;
|
||||
|
||||
if (provider === 'google') {
|
||||
const result = createGoogleAuthUrl();
|
||||
if (!result) throw error(404, 'Google OAuth not configured');
|
||||
|
||||
cookies.set('oauth_state', result.state, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 600
|
||||
});
|
||||
cookies.set('oauth_code_verifier', result.codeVerifier, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 600
|
||||
});
|
||||
|
||||
throw redirect(302, result.url.toString());
|
||||
}
|
||||
|
||||
if (provider === 'github') {
|
||||
const result = createGithubAuthUrl();
|
||||
if (!result) throw error(404, 'GitHub OAuth not configured');
|
||||
|
||||
cookies.set('oauth_state', result.state, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 600
|
||||
});
|
||||
|
||||
throw redirect(302, result.url.toString());
|
||||
}
|
||||
|
||||
if (provider === 'oidc') {
|
||||
const result = await createOidcAuthUrl();
|
||||
if (!result) throw error(404, 'OIDC not configured');
|
||||
|
||||
cookies.set('oauth_state', result.state, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 600
|
||||
});
|
||||
cookies.set('oauth_code_verifier', result.codeVerifier, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 600
|
||||
});
|
||||
|
||||
throw redirect(302, result.url.toString());
|
||||
}
|
||||
|
||||
throw error(404, 'Unknown provider');
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { validateGoogleCallback } from '$lib/server/oauth/google.js';
|
||||
import { validateGithubCallback } from '$lib/server/oauth/github.js';
|
||||
import { validateOidcCallback } from '$lib/server/oauth/oidc.js';
|
||||
import { findOrLinkOAuthUser, createSession } from '$lib/server/auth.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies, request, getClientAddress }) => {
|
||||
const { provider } = params;
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const storedState = cookies.get('oauth_state');
|
||||
|
||||
if (!code || !state || !storedState || state !== storedState) {
|
||||
throw error(400, 'Invalid OAuth callback');
|
||||
}
|
||||
|
||||
// Clean up OAuth cookies
|
||||
cookies.delete('oauth_state', { path: '/' });
|
||||
|
||||
let profile: { email: string; name: string; providerId: string } | null = null;
|
||||
|
||||
if (provider === 'google') {
|
||||
const codeVerifier = cookies.get('oauth_code_verifier');
|
||||
cookies.delete('oauth_code_verifier', { path: '/' });
|
||||
if (!codeVerifier) throw error(400, 'Missing code verifier');
|
||||
profile = await validateGoogleCallback(code, codeVerifier);
|
||||
} else if (provider === 'github') {
|
||||
profile = await validateGithubCallback(code);
|
||||
} else if (provider === 'oidc') {
|
||||
const codeVerifier = cookies.get('oauth_code_verifier');
|
||||
cookies.delete('oauth_code_verifier', { path: '/' });
|
||||
if (!codeVerifier) throw error(400, 'Missing code verifier');
|
||||
profile = await validateOidcCallback(code, codeVerifier);
|
||||
} else {
|
||||
throw error(404, 'Unknown provider');
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
throw error(400, 'Failed to get profile from OAuth provider');
|
||||
}
|
||||
|
||||
// Find or link user (invite-only)
|
||||
const user = findOrLinkOAuthUser(provider, profile.providerId, profile.email, profile.name);
|
||||
if (!user) {
|
||||
// User not found — not invited
|
||||
throw redirect(302, '/login?error=no_account');
|
||||
}
|
||||
|
||||
// Create session
|
||||
const userAgent = request.headers.get('user-agent') || undefined;
|
||||
const ip = getClientAddress();
|
||||
const token = await createSession(user.id, { userAgent, ip });
|
||||
|
||||
cookies.set('session', token, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 30 * 24 * 60 * 60
|
||||
});
|
||||
|
||||
throw redirect(302, '/');
|
||||
};
|
||||
Reference in New Issue
Block a user