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:
@@ -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',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
103
server/utils/test-helpers.ts
Normal file
103
server/utils/test-helpers.ts
Normal 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',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user