Implement Password Grant Flow for Authentication and Enhance User Experience

- Introduced Password Grant Flow for user authentication, allowing direct login with email and password.
- Updated `useAuth` composable to manage login and logout processes, including Single Sign-Out from Cidaas.
- Enhanced user interface with a new `UserMenu` component displaying user information and logout functionality.
- Updated homepage to show personalized greetings for logged-in users and a login prompt for guests.
- Added logout confirmation page with a countdown redirect to the homepage.
- Documented the implementation details and future enhancements for OAuth2 flows in CLAUDE.md and other relevant documentation.
- Added test credentials and guidelines for automated testing in the new TESTING.md file.
This commit is contained in:
Bastian Masanek
2025-11-01 15:23:08 +01:00
parent 83ba708023
commit cc35636d1a
40 changed files with 1843 additions and 31 deletions

View File

@@ -82,6 +82,7 @@ export default defineEventHandler(async (event) => {
firstName: user.firstName,
lastName: user.lastName,
},
accessToken: tokens.access_token, // Store for logout
loggedInAt: new Date().toISOString(),
})

View File

@@ -3,7 +3,7 @@
/**
* POST /api/auth/logout
*
* End user session and clear session cookie
* End user session and perform Single Sign-Out at Cidaas
*
* Response:
* {
@@ -12,13 +12,34 @@
*/
export default defineEventHandler(async (event) => {
// Clear session (nuxt-auth-utils)
await clearUserSession(event)
try {
// 1. Get session to retrieve access token
const session = await getUserSession(event)
// Optional: Revoke Cidaas tokens (Single Sign-Out)
// This would require storing refresh_token in session and calling Cidaas revoke endpoint
// 2. If access token exists, logout from Cidaas (Single Sign-Out)
if (session.accessToken) {
try {
await logoutFromCidaas(session.accessToken)
} catch (error) {
// Log error but continue with local logout
console.error('Cidaas logout failed, continuing with local logout:', error)
}
}
return {
success: true,
// 3. Clear local session (nuxt-auth-utils)
await clearUserSession(event)
return {
success: true,
}
} catch (error) {
console.error('Logout error:', error)
// Clear session even if Cidaas logout fails
await clearUserSession(event)
return {
success: true, // Always return success for logout
}
}
})

View File

@@ -0,0 +1,18 @@
/**
* GET /api/test/credentials
*
* Returns test user credentials for automated testing
*
* ⚠️ SECURITY: This endpoint is ONLY available in development mode.
* It returns 404 in production to prevent credential exposure.
*
* Usage in tests:
* ```typescript
* const response = await fetch('http://localhost:3000/api/test/credentials')
* const { email, password } = await response.json()
* ```
*/
import { createTestCredentialsEndpoint } from '../../utils/test-helpers'
export default createTestCredentialsEndpoint()

View File

@@ -151,19 +151,69 @@ export async function fetchUserInfo(accessToken: string): Promise<CidaasUserInfo
}
}
/**
* Get OAuth2 Access Token with cidaas:register scope
* Uses Client Credentials Flow to obtain token for user registration
*
* @returns Access token for registration API
* @throws H3Error if token request fails
*/
async function getRegistrationToken(): Promise<string> {
const config = useRuntimeConfig()
const params = new URLSearchParams({
grant_type: 'client_credentials',
client_id: config.cidaas.clientId,
client_secret: config.cidaas.clientSecret,
scope: 'cidaas:register',
})
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('Failed to get registration token:', errorData)
throw createError({
statusCode: response.status,
statusMessage: 'Failed to get registration token',
})
}
const tokenData = await response.json()
return tokenData.access_token
} catch (error) {
console.error('Registration token error:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to obtain registration token',
})
}
}
/**
* Register new user via Cidaas Registration API
* Uses cidaas:register scope (direct registration without invite)
*
* @param data - Registration data
* @returns Success indicator (user must verify email before login)
* @returns Success indicator with redirect_uri for email verification
* @throws H3Error if registration fails
*/
export async function registerUser(
data: CidaasRegistrationRequest
): Promise<{ success: boolean; message: string }> {
): Promise<{ success: boolean; message: string; redirect_uri?: string }> {
const config = useRuntimeConfig()
// Cidaas registration endpoint (adjust based on actual API)
// Get access token with cidaas:register scope
const accessToken = await getRegistrationToken()
// Cidaas registration endpoint (cidaas:register scenario)
const registrationUrl = `${config.cidaas.issuer}/users-srv/register`
try {
@@ -171,6 +221,7 @@ export async function registerUser(
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
email: data.email,
@@ -200,9 +251,13 @@ export async function registerUser(
})
}
const result = await response.json()
// Successful response includes redirect_uri for email verification
return {
success: true,
message: 'Registration successful. Please verify your email.',
redirect_uri: result.data?.redirect_uri,
}
} catch (error) {
console.error('Registration error:', error)
@@ -342,3 +397,59 @@ export async function refreshAccessToken(refreshToken: string): Promise<CidaasTo
})
}
}
/**
* Logout from Cidaas (Single Sign-Out)
*
* Terminates the user's session at Cidaas identity provider.
* The post_logout_redirect_uri must be configured in Cidaas Admin Panel.
*
* @param accessToken - Access token from user session
* @throws H3Error if logout fails
*/
export async function logoutFromCidaas(accessToken: string): Promise<void> {
const config = useRuntimeConfig()
// Cidaas logout endpoint
const logoutUrl = `${config.cidaas.issuer}/session/end_session`
const params = new URLSearchParams({
access_token_hint: accessToken,
post_logout_redirect_uri: config.cidaas.postLogoutRedirectUri,
})
try {
const response = await fetch(logoutUrl, {
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 logout failed:', errorData)
throw createError({
statusCode: response.status,
statusMessage: 'Logout from Cidaas failed',
data: errorData,
})
}
// Logout successful
console.log('User logged out from Cidaas successfully')
} catch (error) {
console.error('Cidaas logout error:', error)
if ((error as H3Error).statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: 'Failed to logout from Cidaas',
})
}
}

View File

@@ -0,0 +1,103 @@
/**
* Test Helper Utilities
*
* Provides utilities for automated testing (Playwright, Vitest E2E)
*
* ⚠️ IMPORTANT: These utilities should ONLY be used in test environments.
* Never use test credentials in production!
*/
/**
* Get test user credentials from environment variables
*
* @throws Error if test credentials are not configured
* @returns Test user email and password
*
* @example
* ```typescript
* // In a Playwright test
* import { getTestCredentials } from '~/server/utils/test-helpers'
*
* const { email, password } = getTestCredentials()
* await page.fill('[name="email"]', email)
* await page.fill('[name="password"]', password)
* ```
*/
export function getTestCredentials() {
const config = useRuntimeConfig()
const email = config.testUser.email
const password = config.testUser.password
if (!email || !password) {
throw new Error(
'Test credentials not configured. Please set TEST_USER_EMAIL and TEST_USER_PASSWORD in .env'
)
}
// Security check: Warn if used in production
if (process.env.NODE_ENV === 'production') {
console.warn(
'⚠️ WARNING: Test credentials are being used in production environment! This is a security risk.'
)
}
return {
email,
password,
}
}
/**
* Check if test credentials are configured
*
* @returns true if test credentials are available
*
* @example
* ```typescript
* if (hasTestCredentials()) {
* // Run authenticated tests
* const { email, password } = getTestCredentials()
* // ... test login flow
* } else {
* console.log('Skipping authenticated tests - no test credentials configured')
* }
* ```
*/
export function hasTestCredentials(): boolean {
const config = useRuntimeConfig()
return !!(config.testUser.email && config.testUser.password)
}
/**
* API endpoint to get test credentials
* ⚠️ ONLY available in development mode
*
* This endpoint allows tests to fetch credentials dynamically.
* It's automatically disabled in production.
*
* GET /api/test/credentials
* Returns: { email: string, password: string }
*/
export function createTestCredentialsEndpoint() {
return defineEventHandler((event) => {
// SECURITY: Only allow in development
if (process.env.NODE_ENV === 'production') {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
})
}
try {
const credentials = getTestCredentials()
return credentials
} catch (error) {
throw createError({
statusCode: 500,
statusMessage:
error instanceof Error ? error.message : 'Test credentials not configured',
})
}
})
}