You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
128 lines
3.4 KiB
128 lines
3.4 KiB
// 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')
|
|
}
|
|
})
|
|
|