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

@@ -218,6 +218,74 @@ export async function registerUser(
}
}
/**
* Login with username and password (Resource Owner Password Credentials Flow)
*
* @param email - User email address
* @param password - User password
* @returns Token response with access_token and id_token
* @throws H3Error if login fails
*/
export async function loginWithPassword(
email: string,
password: string
): Promise<CidaasTokenResponse> {
const config = useRuntimeConfig()
// Prepare token request with password grant
const params = new URLSearchParams({
grant_type: 'password',
username: email, // Cidaas uses 'username' field for email
password,
client_id: config.cidaas.clientId,
client_secret: config.cidaas.clientSecret,
scope: 'openid profile email', // Request OIDC scopes
})
try {
const response = await fetch(config.cidaas.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
console.error('Cidaas password login failed:', errorData)
// Handle specific errors
if (response.status === 401) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid email or password',
})
}
throw createError({
statusCode: response.status,
statusMessage: 'Login failed',
data: errorData,
})
}
const tokens: CidaasTokenResponse = await response.json()
return tokens
} catch (error) {
console.error('Password login error:', error)
if ((error as H3Error).statusCode) {
throw error // Re-throw H3Error
}
throw createError({
statusCode: 500,
statusMessage: 'Failed to authenticate with Cidaas',
})
}
}
/**
* Refresh access token using refresh token
*

View File

@@ -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)
}