// server/utils/cidaas.ts /** * Cidaas API Client for OAuth2/OIDC integration * * Provides functions to interact with Cidaas endpoints: * - Token exchange (authorization code → access/ID tokens) * - UserInfo fetch * - User registration */ import type { H3Error } from 'h3' /** * Cidaas Token Response */ export interface CidaasTokenResponse { access_token: string token_type: string expires_in: number refresh_token?: string id_token: string // JWT with user identity scope: string } /** * Cidaas UserInfo Response */ export interface CidaasUserInfo { sub: string // Unique user ID (experimenta_id) email: string email_verified: boolean given_name?: string family_name?: string name?: string phone_number?: string updated_at?: number } /** * Cidaas Registration Request */ export interface CidaasRegistrationRequest { email: string password: string given_name: string family_name: string locale?: string } /** * Exchange authorization code for access/ID tokens * * @param code - Authorization code from callback * @param codeVerifier - PKCE code verifier * @returns Token response * @throws H3Error if exchange fails */ export async function exchangeCodeForToken( code: string, codeVerifier: string ): Promise { const config = useRuntimeConfig() // Prepare token request const params = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: config.cidaas.redirectUri, client_id: config.cidaas.clientId, client_secret: config.cidaas.clientSecret, code_verifier: codeVerifier, // PKCE proof }) 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 token exchange failed:', errorData) throw createError({ statusCode: response.status, statusMessage: 'Token exchange failed', data: errorData, }) } const tokens: CidaasTokenResponse = await response.json() return tokens } catch (error) { console.error('Token exchange error:', error) if ((error as H3Error).statusCode) { throw error // Re-throw H3Error } throw createError({ statusCode: 500, statusMessage: 'Failed to exchange authorization code', }) } } /** * Fetch user info from Cidaas UserInfo endpoint * * @param accessToken - OAuth2 access token * @returns User profile data * @throws H3Error if fetch fails */ export async function fetchUserInfo(accessToken: string): Promise { const config = useRuntimeConfig() try { const response = await fetch(config.cidaas.userinfoUrl, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, }, }) if (!response.ok) { console.error('Cidaas UserInfo fetch failed:', response.status) throw createError({ statusCode: response.status, statusMessage: 'Failed to fetch user info', }) } const userInfo: CidaasUserInfo = await response.json() return userInfo } catch (error) { console.error('UserInfo fetch error:', error) if ((error as H3Error).statusCode) { throw error } throw createError({ statusCode: 500, statusMessage: 'Failed to fetch user information', }) } } /** * 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 { 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 with redirect_uri for email verification * @throws H3Error if registration fails */ export async function registerUser( data: CidaasRegistrationRequest ): Promise<{ success: boolean; message: string; redirect_uri?: string }> { const config = useRuntimeConfig() // 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 { const response = await fetch(registrationUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ email: data.email, password: data.password, given_name: data.given_name, family_name: data.family_name, locale: data.locale || 'de', }), }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) console.error('Cidaas registration failed:', errorData) // Handle specific errors if (response.status === 409) { throw createError({ statusCode: 409, statusMessage: 'Email already registered', }) } throw createError({ statusCode: response.status, statusMessage: 'Registration failed', data: errorData, }) } 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) if ((error as H3Error).statusCode) { throw error } throw createError({ statusCode: 500, statusMessage: 'Failed to register user', }) } } /** * 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 { 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 // Cidaas returns 400 with error: 'invalid_username_password' for invalid credentials if (response.status === 400 && errorData.error === 'invalid_username_password') { throw createError({ statusCode: 401, statusMessage: 'Invalid email or password', }) } 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 * * @param refreshToken - Refresh token from previous login * @returns New token response * @throws H3Error if refresh fails */ export async function refreshAccessToken(refreshToken: string): Promise { const config = useRuntimeConfig() const params = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: config.cidaas.clientId, client_secret: config.cidaas.clientSecret, }) try { const response = await fetch(config.cidaas.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: params.toString(), }) if (!response.ok) { throw createError({ statusCode: response.status, statusMessage: 'Token refresh failed', }) } const tokens: CidaasTokenResponse = await response.json() return tokens } catch (error) { console.error('Token refresh error:', error) if ((error as H3Error).statusCode) { throw error } throw createError({ statusCode: 500, statusMessage: 'Failed to refresh token', }) } } /** * 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 { 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', }) } }