Implement direct login functionality with email and password

- Update login API to support direct authentication via email and password, removing the OAuth2 redirect flow.
- Modify LoginForm component to include password field and validation.
- Refactor useAuth composable to handle login with both email and password.
- Remove unnecessary OAuth2 callback handler and PKCE utilities.
- Update relevant documentation and error handling for the new login method.
This commit is contained in:
Bastian Masanek
2025-10-31 14:27:38 +01:00
parent e71316dfe7
commit 7c7c4fcb6f
8 changed files with 178 additions and 268 deletions

View File

@@ -1,128 +0,0 @@
// server/api/auth/callback.get.ts
/**
* GET /api/auth/callback
*
* OAuth2 callback handler - receives authorization code from Cidaas
*
* Query params:
* - code: Authorization code
* - state: CSRF protection token
*
* Flow:
* 1. Validate state parameter
* 2. Exchange code for tokens
* 3. Validate ID token
* 4. Fetch user info
* 5. Create/update user in PostgreSQL
* 6. Create session
* 7. Redirect to homepage
*/
import { eq } from 'drizzle-orm'
import { users } from '../../database/schema'
export default defineEventHandler(async (event) => {
// 1. Extract query parameters
const query = getQuery(event)
const { code, state } = query
if (!code || !state) {
throw createError({
statusCode: 400,
statusMessage: 'Missing code or state parameter',
})
}
// 2. Validate state (CSRF protection)
const storedState = getCookie(event, 'oauth_state')
if (!storedState || state !== storedState) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid state parameter - possible CSRF attack',
})
}
// 3. Retrieve PKCE verifier
const verifier = getCookie(event, 'pkce_verifier')
if (!verifier) {
throw createError({
statusCode: 400,
statusMessage: 'PKCE verifier not found - session expired',
})
}
try {
// 4. Exchange authorization code for tokens
const tokens = await exchangeCodeForToken(code as string, verifier)
// 5. Validate ID token (JWT)
const idTokenPayload = await verifyIdToken(tokens.id_token)
// 6. Fetch detailed user info from Cidaas
const cidaasUser = await fetchUserInfo(tokens.access_token)
// 7. Get database instance
const db = useDatabase()
// 8. Check if user already exists in our database
let user = await db.query.users.findFirst({
where: eq(users.experimentaId, cidaasUser.sub),
})
if (!user) {
// First time login - create new user
const [newUser] = await db
.insert(users)
.values({
experimentaId: cidaasUser.sub, // Cidaas user ID
email: cidaasUser.email,
firstName: cidaasUser.given_name || null,
lastName: cidaasUser.family_name || null,
})
.returning()
user = newUser
console.log('New user created:', user.id)
} else {
// Existing user - update last login timestamp
await db.update(users).set({ updatedAt: new Date() }).where(eq(users.id, user.id))
console.log('User logged in:', user.id)
}
// 9. Create encrypted session (nuxt-auth-utils)
await setUserSession(event, {
user: {
id: user.id,
experimentaId: user.experimentaId,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
},
loggedInAt: new Date().toISOString(),
})
// 10. Clean up temporary cookies
deleteCookie(event, 'oauth_state')
deleteCookie(event, 'pkce_verifier')
// 11. Redirect to homepage (or original requested page)
const redirectTo = getCookie(event, 'redirect_after_login') || '/'
deleteCookie(event, 'redirect_after_login')
return sendRedirect(event, redirectTo)
} catch (error) {
console.error('OAuth callback error:', error)
// Clean up cookies on error
deleteCookie(event, 'oauth_state')
deleteCookie(event, 'pkce_verifier')
// Redirect to login page with error
return sendRedirect(event, '/auth?error=login_failed')
}
})

View File

@@ -3,71 +3,105 @@
/**
* POST /api/auth/login
*
* Initiates OAuth2 Authorization Code Flow with PKCE
* Direct login with email and password (no OAuth2 redirect)
*
* Request body:
* {
* "email": "user@example.com"
* "email": "user@example.com",
* "password": "SecureP@ssw0rd"
* }
*
* Response:
* {
* "redirectUrl": "https://experimenta.cidaas.de/authz-srv/authz?..."
* "success": true
* }
*
* Client should redirect user to redirectUrl
* Creates session cookie on success
*/
import { z } from 'zod'
import { eq } from 'drizzle-orm'
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(1, 'Password is required'),
})
export default defineEventHandler(async (event) => {
// 1. Validate request body
const body = await readBody(event)
const { email } = loginSchema.parse(body)
const { email, password } = loginSchema.parse(body)
// 2. Generate PKCE challenge
const { verifier, challenge } = await generatePKCE()
try {
// 2. Authenticate with Cidaas (Resource Owner Password Credentials flow)
const tokens = await loginWithPassword(email, password)
// 3. Generate state for CSRF protection
const state = generateState(32)
// 3. Validate ID token (JWT)
const idTokenPayload = await verifyIdToken(tokens.id_token)
// 4. Store PKCE verifier in encrypted cookie (5 min TTL)
setCookie(event, 'pkce_verifier', verifier, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 300, // 5 minutes
path: '/',
})
// 4. Fetch user info from Cidaas
const cidaasUser = await fetchUserInfo(tokens.access_token)
// 5. Store state in cookie for validation
setCookie(event, 'oauth_state', state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 300, // 5 minutes
path: '/',
})
// 5. Create/update user in local database
const db = useDatabase()
// 6. Build Cidaas authorization URL
const config = useRuntimeConfig()
const authUrl = new URL(config.cidaas.authorizeUrl)
let user = await db.query.users.findFirst({
where: eq(users.experimentaId, cidaasUser.sub),
})
authUrl.searchParams.set('client_id', config.cidaas.clientId)
authUrl.searchParams.set('redirect_uri', config.cidaas.redirectUri)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('scope', 'openid profile email')
authUrl.searchParams.set('state', state)
authUrl.searchParams.set('code_challenge', challenge)
authUrl.searchParams.set('code_challenge_method', 'S256')
authUrl.searchParams.set('login_hint', email) // Pre-fill email in Cidaas form
if (!user) {
// First time login - create user profile
const [newUser] = await db
.insert(users)
.values({
experimentaId: cidaasUser.sub,
email: cidaasUser.email,
firstName: cidaasUser.given_name || '',
lastName: cidaasUser.family_name || '',
})
.returning()
// 7. Return redirect URL to client
return {
redirectUrl: authUrl.toString(),
user = newUser
} else {
// Update last login timestamp
await db
.update(users)
.set({
updatedAt: new Date(),
})
.where(eq(users.id, user.id))
}
// 6. Create encrypted session
await setUserSession(event, {
user: {
id: user.id,
experimentaId: user.experimentaId,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
},
loggedInAt: new Date().toISOString(),
})
// 7. Return success
return {
success: true,
}
} catch (error: any) {
console.error('Login error:', error)
// Handle specific error cases
if (error.statusCode === 401) {
throw createError({
statusCode: 401,
statusMessage: 'Ungültige E-Mail-Adresse oder Passwort',
})
}
throw createError({
statusCode: 500,
statusMessage: 'Anmeldung fehlgeschlagen',
})
}
})