Skip to content

Commit

Permalink
SSO flow implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
aliirz committed Nov 6, 2024
1 parent 6e79320 commit 1b9fd9e
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 66 deletions.
24 changes: 13 additions & 11 deletions app/api/auth/authorize/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { NextResponse } from 'next/server'
import { createLoginUrl } from '@/lib/keycloak-config'
import { cookies } from 'next/headers'

export async function GET(request: Request) {
try {
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(
Expand All @@ -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(
Expand Down
32 changes: 21 additions & 11 deletions app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -24,34 +22,46 @@ 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)
throw new Error(`Authentication failed: ${errorText}`)
}

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)
Expand Down
41 changes: 37 additions & 4 deletions app/api/auth/userinfo/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
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
const authHeader = request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json(
{ message: 'Missing or invalid token' },
{ status: 401 }
{
status: 401,
headers: {
'Access-Control-Allow-Origin': allowedOrigin,
}
}
)
}

Expand All @@ -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,
}
}
)
}

Expand Down Expand Up @@ -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,
}
}
)
}
}
}
1 change: 1 addition & 0 deletions app/api/userinfo/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

73 changes: 33 additions & 40 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -84,7 +81,7 @@ export default function LoginPage() {
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="flex items-center justify-center">
<CardTitle className="text-center">
{serviceName ? (
<>Login to access {serviceName}</>
) : (
Expand All @@ -100,39 +97,35 @@ export default function LoginPage() {
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">
Email address
</label>
<Label htmlFor="username">Username</Label>
<Input
id="email"
type="email"
id="username"
type="text"
value={formData.username}
onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))}
placeholder="Enter your email"
onChange={(e) => setFormData(prev => ({
...prev,
username: e.target.value
}))}
required
/>
</div>
<div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium">
Password
</label>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="Enter your password"
onChange={(e) => setFormData(prev => ({
...prev,
password: e.target.value
}))}
required
/>
</div>
</CardContent>
<CardFooter className="flex flex-col space-y-4">
<Button
type="submit"
className="w-full bg-primary text-primary-foreground hover:bg-primary/90"
disabled={isLoading}
>
{isLoading ? "Logging in..." : "Login"}
<CardFooter>
<Button type="submit" className="w-full">
Login
</Button>
</CardFooter>
</form>
Expand Down

0 comments on commit 1b9fd9e

Please sign in to comment.