Browse Source
- 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.main
8 changed files with 178 additions and 268 deletions
@ -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') |
|
||||
} |
|
||||
}) |
|
||||
@ -1,90 +0,0 @@ |
|||||
// server/utils/pkce.ts
|
|
||||
|
|
||||
/** |
|
||||
* PKCE (Proof Key for Code Exchange) utilities for OAuth2 security. |
|
||||
* |
|
||||
* PKCE prevents authorization code interception attacks by requiring |
|
||||
* the client to prove possession of the original code verifier. |
|
||||
* |
|
||||
* Flow: |
|
||||
* 1. Generate random code_verifier (43-128 chars) |
|
||||
* 2. Hash verifier with SHA-256 → code_challenge |
|
||||
* 3. Send challenge to authorization server |
|
||||
* 4. Server returns authorization code |
|
||||
* 5. Exchange code + verifier for tokens |
|
||||
* 6. Server validates: SHA256(verifier) === stored_challenge |
|
||||
*/ |
|
||||
|
|
||||
/** |
|
||||
* Generate a random code verifier (43-128 URL-safe characters) |
|
||||
* |
|
||||
* @param length - Length of verifier (default: 64) |
|
||||
* @returns Base64URL-encoded random string |
|
||||
*/ |
|
||||
export function generateCodeVerifier(length: number = 64): string { |
|
||||
// Generate random bytes
|
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(length)) |
|
||||
|
|
||||
// Convert to base64url (URL-safe base64 without padding)
|
|
||||
return base64UrlEncode(randomBytes) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Generate SHA-256 hash of code verifier → code challenge |
|
||||
* |
|
||||
* @param verifier - The code verifier |
|
||||
* @returns Base64URL-encoded SHA-256 hash |
|
||||
*/ |
|
||||
export async function generateCodeChallenge(verifier: string): Promise<string> { |
|
||||
// Convert verifier string to Uint8Array
|
|
||||
const encoder = new TextEncoder() |
|
||||
const data = encoder.encode(verifier) |
|
||||
|
|
||||
// Hash with SHA-256
|
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data) |
|
||||
|
|
||||
// Convert to base64url
|
|
||||
return base64UrlEncode(new Uint8Array(hashBuffer)) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Generate PKCE verifier + challenge pair |
|
||||
* |
|
||||
* @returns Object with verifier and challenge |
|
||||
*/ |
|
||||
export async function generatePKCE(): Promise<{ |
|
||||
verifier: string |
|
||||
challenge: string |
|
||||
}> { |
|
||||
const verifier = generateCodeVerifier() |
|
||||
const challenge = await generateCodeChallenge(verifier) |
|
||||
|
|
||||
return { verifier, challenge } |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Convert Uint8Array to Base64URL string |
|
||||
* |
|
||||
* Base64URL is URL-safe variant of Base64: |
|
||||
* - Replace '+' with '-' |
|
||||
* - Replace '/' with '_' |
|
||||
* - Remove padding '=' |
|
||||
*/ |
|
||||
function base64UrlEncode(buffer: Uint8Array): string { |
|
||||
// Convert to base64
|
|
||||
const base64 = btoa(String.fromCharCode(...buffer)) |
|
||||
|
|
||||
// Convert to base64url
|
|
||||
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Generate random state parameter for CSRF protection |
|
||||
* |
|
||||
* @param length - Length of state string (default: 32) |
|
||||
* @returns Random URL-safe string |
|
||||
*/ |
|
||||
export function generateState(length: number = 32): string { |
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(length)) |
|
||||
return base64UrlEncode(randomBytes) |
|
||||
} |
|
||||
Loading…
Reference in new issue