From 1b9fd9eeb91c92d0062729479d900edee32f2920 Mon Sep 17 00:00:00 2001 From: aliirz Date: Wed, 6 Nov 2024 17:29:16 +0500 Subject: [PATCH] SSO flow implemented --- app/api/auth/authorize/route.ts | 24 ++++++----- app/api/auth/login/route.ts | 32 ++++++++++----- app/api/auth/userinfo/route.ts | 41 ++++++++++++++++-- app/api/userinfo/route.ts | 1 + app/login/page.tsx | 73 +++++++++++++++------------------ 5 files changed, 105 insertions(+), 66 deletions(-) create mode 100644 app/api/userinfo/route.ts diff --git a/app/api/auth/authorize/route.ts b/app/api/auth/authorize/route.ts index bf77cf1..77d9c20 100644 --- a/app/api/auth/authorize/route.ts +++ b/app/api/auth/authorize/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from 'next/server' -import { createLoginUrl } from '@/lib/keycloak-config' import { cookies } from 'next/headers' export async function GET(request: Request) { @@ -7,6 +6,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url) const clientId = searchParams.get('client_id') const redirectUri = searchParams.get('redirect_uri') + const serviceName = searchParams.get('service_name') if (!clientId || !redirectUri) { return NextResponse.json( @@ -15,27 +15,29 @@ export async function GET(request: Request) { ) } - // Generate a state parameter to prevent CSRF - const state = Math.random().toString(36).substring(7) - - // Store the original redirect URI and client ID in cookies - const response = NextResponse.redirect(createLoginUrl(state)) - - response.cookies.set('oauth_state', state, { + // Store client information in cookies for later use + const cookieStore = cookies() + cookieStore.set('oauth_client_id', clientId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 3600 // 1 hour }) - - response.cookies.set('oauth_redirect_uri', redirectUri, { + + cookieStore.set('oauth_redirect_uri', redirectUri, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 3600 }) - return response + // Redirect to login page with service name + const loginUrl = new URL('/login', request.url) + if (serviceName) { + loginUrl.searchParams.set('service_name', serviceName) + } + + return NextResponse.redirect(loginUrl) } catch (error) { console.error('Authorization error:', error) return NextResponse.json( diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index af30bbe..52ad7e5 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -3,11 +3,9 @@ import { KEYCLOAK_CONFIG, KEYCLOAK_URLS } from '@/lib/keycloak-config' export async function POST(request: Request) { try { - const { username, password } = await request.json() + const { username, password, clientId, redirectUri } = await request.json() - console.log('Login attempt for:', username) - console.log('Keycloak URL:', KEYCLOAK_URLS.TOKEN) - console.log('Client ID:', KEYCLOAK_CONFIG.CLIENT_ID) + console.log('Login request:', { username, clientId, redirectUri }) // Debug log const tokenResponse = await fetch(KEYCLOAK_URLS.TOKEN, { method: 'POST', @@ -24,8 +22,6 @@ export async function POST(request: Request) { }), }) - console.log('Keycloak response status:', tokenResponse.status) - if (!tokenResponse.ok) { const errorText = await tokenResponse.text() console.error('Keycloak error response:', errorText) @@ -33,25 +29,39 @@ export async function POST(request: Request) { } const tokens = await tokenResponse.json() - console.log('Token received:', tokens.access_token ? 'Yes' : 'No') - const apiResponse = NextResponse.json({ isAuthenticated: true }) + // If this is an SSO login request + if (clientId && redirectUri) { + console.log('Processing SSO redirect to:', redirectUri) + + // Create the redirect URL with tokens + const finalRedirectUrl = new URL(redirectUri) + finalRedirectUrl.searchParams.set('access_token', tokens.access_token) + finalRedirectUrl.searchParams.set('id_token', tokens.id_token || '') + + return NextResponse.json({ + redirect: finalRedirectUrl.toString() + }) + } + + // Regular Pehchan login + const response = NextResponse.json({ isAuthenticated: true }) - apiResponse.cookies.set('access_token', tokens.access_token, { + response.cookies.set('access_token', tokens.access_token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: tokens.expires_in }) - apiResponse.cookies.set('refresh_token', tokens.refresh_token, { + response.cookies.set('refresh_token', tokens.refresh_token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: tokens.refresh_expires_in }) - return apiResponse + return response } catch (error) { console.error('Login error:', error) diff --git a/app/api/auth/userinfo/route.ts b/app/api/auth/userinfo/route.ts index 24f6dbc..38156dc 100644 --- a/app/api/auth/userinfo/route.ts +++ b/app/api/auth/userinfo/route.ts @@ -1,6 +1,20 @@ import { NextResponse } from 'next/server' import { supabase } from '@/lib/supabase' +const allowedOrigin = process.env.NODE_ENV === 'production' + ? process.env.FBR_PORTAL_URL ?? 'https://default-production-url.com' + : 'http://localhost:3001' + +export async function OPTIONS() { + return new NextResponse(null, { + headers: { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }) +} + export async function GET(request: Request) { try { // Get the access token from Authorization header @@ -8,7 +22,12 @@ export async function GET(request: Request) { if (!authHeader?.startsWith('Bearer ')) { return NextResponse.json( { message: 'Missing or invalid token' }, - { status: 401 } + { + status: 401, + headers: { + 'Access-Control-Allow-Origin': allowedOrigin, + } + } ) } @@ -27,7 +46,12 @@ export async function GET(request: Request) { if (!userInfoResponse.ok) { return NextResponse.json( { message: 'Invalid token' }, - { status: 401 } + { + status: 401, + headers: { + 'Access-Control-Allow-Origin': allowedOrigin, + } + } ) } @@ -60,13 +84,22 @@ export async function GET(request: Request) { phone: supabaseUser?.phone, // Add any other relevant fields } + }, { + headers: { + 'Access-Control-Allow-Origin': allowedOrigin, + } }) } catch (error) { console.error('UserInfo error:', error) return NextResponse.json( { message: 'Failed to fetch user information' }, - { status: 500 } + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': allowedOrigin, + } + } ) } -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/api/userinfo/route.ts b/app/api/userinfo/route.ts new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/app/api/userinfo/route.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx index 3099323..408566b 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,12 +1,12 @@ "use client" -import { useState, useEffect } from 'react' +import { useEffect, useState } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { useToast } from "@/hooks/use-toast" - +import { Label } from "@/components/ui/label" export default function LoginPage() { const router = useRouter() const { toast } = useToast() @@ -36,44 +36,41 @@ export default function LoginPage() { setIsLoading(true) try { - // First, validate email format - if (!formData.username.includes('@')) { - throw new Error('Please enter a valid email address') - } - const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(formData), + body: JSON.stringify({ + username: formData.username, + password: formData.password, + ...(clientId && { clientId }), + ...(redirectUri && { redirectUri }), + }), }) const data = await response.json() if (!response.ok) { - // Handle specific Keycloak errors - if (data.error === 'invalid_grant') { - throw new Error('Invalid email or password') - } - if (data.error === 'invalid_client') { - throw new Error('Authentication service error. Please try again later.') - } throw new Error(data.message || 'Login failed') } + if (data.redirect) { + window.location.href = data.redirect + return + } + toast({ title: "Success", - description: "Login successful!", + description: "Logged in successfully", }) - router.push('/dashboard') - router.refresh() - } catch (error: any) { + + } catch (error) { toast({ variant: "destructive", title: "Error", - description: error.message, + description: error instanceof Error ? error.message : "Login failed", }) } finally { setIsLoading(false) @@ -84,7 +81,7 @@ export default function LoginPage() {
- + {serviceName ? ( <>Login to access {serviceName} ) : ( @@ -100,39 +97,35 @@ export default function LoginPage() {
- + setFormData(prev => ({ ...prev, username: e.target.value }))} - placeholder="Enter your email" + onChange={(e) => setFormData(prev => ({ + ...prev, + username: e.target.value + }))} required />
- + setFormData(prev => ({ ...prev, password: e.target.value }))} - placeholder="Enter your password" + onChange={(e) => setFormData(prev => ({ + ...prev, + password: e.target.value + }))} required />
- -