Skip to content

Commit 4e316f5

Browse files
Merge pull request #10 from biranchikulesika/fix/auth
feat(auth): comprehensive admin security system with session expiry, passkey implementation, and proper security activity logging.
2 parents cdff5e4 + ba04a53 commit 4e316f5

10 files changed

Lines changed: 700 additions & 134 deletions

File tree

frontend/app/admin/auth/callback/route.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextResponse } from 'next/server';
22
import { createServerClient } from '@supabase/ssr';
33
import { cookies } from 'next/headers';
4+
import { logSecurityEventDirect } from '../../security-actions';
45

56
export async function GET(request: Request) {
67
const requestUrl = new URL(request.url);
@@ -17,6 +18,14 @@ export async function GET(request: Request) {
1718
const { searchParams } = requestUrl;
1819
const code = searchParams.get('code');
1920
const next = searchParams.get('next') || '/admin';
21+
const provider = searchParams.get('provider');
22+
23+
const forwardedFor = request.headers.get('x-forwarded-for');
24+
const ip =
25+
forwardedFor?.split(',')[0].trim() ||
26+
request.headers.get('x-real-ip') ||
27+
'unknown';
28+
const ua = request.headers.get('user-agent');
2029

2130
if (code) {
2231
const cookieStore = await cookies();
@@ -41,13 +50,33 @@ export async function GET(request: Request) {
4150
}
4251
);
4352

44-
const { error } = await supabase.auth.exchangeCodeForSession(code);
45-
if (!error) {
53+
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
54+
if (!error && data.user) {
55+
logSecurityEventDirect(
56+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
57+
process.env.SUPABASE_SERVICE_ROLE_KEY || '',
58+
'oauth_login',
59+
data.user.id,
60+
ip,
61+
ua,
62+
{ status: 'Success', metadata: { provider: provider || 'unknown' } }
63+
).catch(() => {});
64+
4665
const redirectUrl = new URL(next, origin);
4766
return NextResponse.redirect(redirectUrl.toString());
4867
}
68+
69+
// Log failed OAuth exchange
70+
logSecurityEventDirect(
71+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
72+
process.env.SUPABASE_SERVICE_ROLE_KEY || '',
73+
'login_failed',
74+
'00000000-0000-0000-0000-000000000000',
75+
ip,
76+
ua,
77+
{ status: 'Failed', metadata: { method: 'oauth', provider: provider || 'unknown', reason: error?.message || 'code_exchange_failed' } }
78+
).catch(() => {});
4979
}
5080

51-
// Return the user to an error page with instructions
5281
return NextResponse.redirect(new URL('/admin/login?error=auth_failed', origin).toString());
5382
}

frontend/app/admin/login/actions.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { createServerClient } from '@supabase/ssr';
44
import { cookies, headers } from 'next/headers';
5+
import { logSecurityEventDirect } from '../security-actions';
56

67
// In-memory rate limiter.
78
// NOTE: This is suitable for development but is not reliable on
@@ -17,6 +18,8 @@ export async function authenticateUser(email: string, password: string) {
1718
headersList.get('x-real-ip') ||
1819
'unknown';
1920

21+
const ua = headersList.get('user-agent');
22+
2023
const now = Date.now();
2124

2225
const record = rateLimitMap.get(ip);
@@ -40,8 +43,8 @@ export async function authenticateUser(email: string, password: string) {
4043

4144
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
4245
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
46+
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
4347

44-
// Authentication requires Supabase to be configured.
4548
if (!supabaseUrl || !supabaseAnonKey) {
4649
return {
4750
error: {
@@ -70,12 +73,17 @@ export async function authenticateUser(email: string, password: string) {
7073
},
7174
});
7275

73-
const { error } = await supabase.auth.signInWithPassword({
76+
const { data, error } = await supabase.auth.signInWithPassword({
7477
email,
7578
password,
7679
});
7780

7881
if (error) {
82+
logSecurityEventDirect(supabaseUrl, serviceKey || '', 'login_failed', '00000000-0000-0000-0000-000000000000', ip, ua, {
83+
status: 'Failed',
84+
metadata: { method: 'email_password', email, reason: error.message },
85+
}).catch(() => {});
86+
7987
return {
8088
error: {
8189
message: 'Invalid login credentials',
@@ -86,7 +94,15 @@ export async function authenticateUser(email: string, password: string) {
8694
// Reset rate limit after successful authentication.
8795
rateLimitMap.delete(ip);
8896

97+
// Log successful login
98+
if (data.user) {
99+
logSecurityEventDirect(supabaseUrl, serviceKey || '', 'login', data.user.id, ip, ua, {
100+
status: 'Success',
101+
metadata: { method: 'email_password' },
102+
}).catch(() => {});
103+
}
104+
89105
return {
90106
error: null,
91107
};
92-
}
108+
}

frontend/app/admin/login/page.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Logo } from '@/components/ui/logo';
1111
import { useFakeTyping } from '@/hooks/use-fake-typing';
1212
import { useAuthSequence } from '@/hooks/use-auth-sequence';
1313
import { authenticateUser } from './actions';
14+
import { logSecurityEvent } from '../security-actions';
1415

1516

1617

@@ -169,6 +170,7 @@ function LoginContent() {
169170
setFailedAttempts(prev => prev + 1);
170171
setInvalidCredentials(true);
171172
setLoading(false);
173+
logSecurityEvent('login_failed', { metadata: { method: 'oauth', provider, reason: signInError.message } }).catch(() => {});
172174
return;
173175
}
174176
};
@@ -177,12 +179,38 @@ function LoginContent() {
177179
setLoading(true);
178180
clearStatus();
179181

180-
// Standard passkey entry or authentication bypass
181-
const texts = ["I'm Biranchi", "Yes, I'm Biranchi", "I love Biranchi"];
182-
setFailedAttemptText(texts[Math.floor(Math.random() * texts.length)]);
183-
setFailedAttempts(prev => prev + 1);
184-
setInvalidCredentials(true);
185-
setLoading(false);
182+
if (!supabase) {
183+
setEmailError("Supabase authentication is not configured in this environment.");
184+
setLoading(false);
185+
return;
186+
}
187+
188+
try {
189+
const { error: passkeyError } = await supabase.auth.signInWithPasskey();
190+
191+
if (passkeyError) {
192+
showFailureMessage();
193+
setInvalidCredentials(true);
194+
setLoading(false);
195+
logSecurityEvent('login_failed', { metadata: { method: 'passkey', reason: passkeyError.message } }).catch(() => {});
196+
return;
197+
}
198+
199+
logSecurityEvent('passkey_login', { metadata: { method: 'passkey' } }).catch(() => {});
200+
await runSuccessSequence();
201+
router.push('/admin');
202+
router.refresh();
203+
} catch (e: unknown) {
204+
setLoading(false);
205+
if (e instanceof Error && e.name === 'NotAllowedError') {
206+
return;
207+
}
208+
if (e instanceof Error && e.message.includes('does not support WebAuthn')) {
209+
setEmailError('Your browser does not support passkeys.');
210+
return;
211+
}
212+
setInvalidCredentials(true);
213+
}
186214
};
187215

188216
return (

0 commit comments

Comments
 (0)