WellData Implementation Guide
0.1.2 - ci-build
WellData Implementation Guide - Local Development build (v0.1.2) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
This specification describes how to update the existing Verslavingskunde Nederland (VKN) OIDC implementation to automatically create WebIDs for anonymous users through integration with the Athumi Pod Platform API. This enables anonymous users authenticated via the GIDS Anonymous Login system to seamlessly access WellData ecosystem services with their own Solid pods.
The current VKN implementation provides anonymous authentication through the MS Auth middleware using OAuth2 Authorization Code Flow. Users receive anonymous identifiers that allow for recurring identification without revealing personal information. To integrate with the WellData ecosystem, these anonymous users need WebIDs and corresponding Solid pods for data storage and interoperability.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Application │───▶│ MS Auth │───▶│ Enhanced VKN │
│ │ │ Middleware │ │ Backend │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
│
▼
┌─────────────────────┐
│ Athumi Pod │
│ Platform API │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ User's Solid Pod │
└─────────────────────┘
In the sequence diagram below, you can find the complete authentication flow for the Anonymous authentication.
Continue with the existing OAuth2 flow as documented:
After successful authentication and JWT validation, implement the following:
async function handlePostAuthentication(idToken: string) {
try {
// 1. Extract user identifier from JWT
const userInfo = validateAndExtractJWT(idToken);
const anonymousUserId = userInfo.sub; // Anonymous user ID
// 2. Check if WebID already exists
const existingWebId = await checkExistingWebId(anonymousUserId);
if (existingWebId) {
return existingWebId;
}
// 3. Create new WebID and pod
const webId = await createWebIdForAnonymousUser(anonymousUserId);
// 4. Store mapping for future reference
await storeUserWebIdMapping(anonymousUserId, webId);
return webId;
} catch (error) {
console.error('WebID creation failed:', error);
// Decide whether to continue without WebID or fail
throw new Error('WebID creation failed');
}
}
interface PodPlatformConfig {
baseUrl: string; // "https://pod-platform.api.athumi.eu"
clientCredentials: {
clientId: string;
clientSecret: string;
};
correlationIdGenerator: () => string;
}
async function createWebIdForAnonymousUser(anonymousUserId: string): Promise<string> {
// 1. Obtain SOLID OIDC token for citizen authentication
const solidToken = await getSolidOIDCToken(anonymousUserId);
// 2. Call Athumi Pod Platform to create WebID and pod
const webId = await createWebIdViaPodPlatform(solidToken);
return webId.uri;
}
async function createWebIdViaPodPlatform(solidToken: string): Promise<WebId> {
const correlationId = generateCorrelationId();
const response = await fetch(`${POD_PLATFORM_BASE_URL}/v1/webids`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${solidToken}`,
'X-Correlation-Id': correlationId,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`WebID creation failed: ${error.detail}`);
}
return await response.json();
}
The VKN backend needs to generate SOLID OIDC tokens for anonymous users:
async function getSolidOIDCToken(anonymousUserId: string): Promise<string> {
// Generate SOLID OIDC token with anonymous user as subject
// This should include the anonymous user ID in the sub claim
// and conform to SOLID OIDC specifications
const tokenPayload = {
sub: anonymousUserId,
iss: VKN_ISSUER_URL,
aud: POD_PLATFORM_BASE_URL,
exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour
iat: Math.floor(Date.now() / 1000),
webid: `${VKN_BASE_URL}/webid/${anonymousUserId}` // Temporary WebID reference
};
return jwt.sign(tokenPayload, PRIVATE_KEY, { algorithm: 'RS256' });
}
interface UserWebIdMapping {
anonymousUserId: string;
webId: string;
createdAt: Date;
lastAccessed: Date;
}
// Store mapping in secure database
async function storeUserWebIdMapping(
anonymousUserId: string,
webId: string
): Promise<void> {
const mapping: UserWebIdMapping = {
anonymousUserId,
webId,
createdAt: new Date(),
lastAccessed: new Date()
};
await database.userWebIdMappings.create(mapping);
}
// Retrieve existing WebID for user
async function checkExistingWebId(anonymousUserId: string): Promise<string | null> {
const mapping = await database.userWebIdMappings.findOne({
anonymousUserId
});
if (mapping) {
// Update last accessed
await database.userWebIdMappings.updateOne(
{ anonymousUserId },
{ lastAccessed: new Date() }
);
return mapping.webId;
}
return null;
}
Update the existing JWT response to include WebID information:
async function generateEnhancedJWT(originalJWT: any, webId: string): Promise<string> {
const enhancedPayload = {
...originalJWT,
webid: webId, // Add WebID to JWT payload
pod_access: true, // Indicate pod access availability
welldata_integration: true // Flag for WellData ecosystem integration
};
return jwt.sign(enhancedPayload, PRIVATE_KEY, { algorithm: 'RS256' });
}
class WebIdCreationError extends Error {
constructor(message: string, public readonly cause?: Error) {
super(message);
this.name = 'WebIdCreationError';
}
}
async function handleWebIdCreationWithFallback(anonymousUserId: string): Promise<string | null> {
try {
return await createWebIdForAnonymousUser(anonymousUserId);
} catch (error) {
console.error(`WebID creation failed for user ${anonymousUserId}:`, error);
// Log for monitoring and debugging
await logWebIdCreationFailure(anonymousUserId, error);
// Depending on requirements, either:
// 1. Return null and continue without WebID
// 2. Retry with exponential backoff
// 3. Throw error and fail authentication
return null; // Graceful degradation
}
}
Add the following environment variables to the VKN configuration:
# Athumi Pod Platform Configuration
POD_PLATFORM_BASE_URL=https://pod-platform.api.athumi.eu
POD_PLATFORM_CLIENT_ID=your_client_id
POD_PLATFORM_CLIENT_SECRET=your_client_secret
# WebID Creation Settings
WEBID_CREATION_ENABLED=true
WEBID_CREATION_TIMEOUT=10000 # 10 seconds
WEBID_FALLBACK_MODE=graceful # graceful | strict
# VKN Specific Settings
VKN_ISSUER_URL=https://ms-auth.sns.gidsopenstandaarden.org
VKN_BASE_URL=https://your-vkn-backend.example.com
# Database for WebID mappings
WEBID_MAPPING_DB_URL=your_database_connection_string
Modify the existing Step 4 (unpack and validate id_token) to include WebID creation:
async function enhancedStep4(idToken: string): Promise<AuthResult> {
// Original validation
const tokenData = validateJWT(idToken);
// New: WebID creation/retrieval
const webId = await handleWebIdCreationWithFallback(tokenData.sub);
// Enhanced response
const authResult: AuthResult = {
userId: tokenData.sub,
webId: webId,
originalToken: idToken,
enhancedToken: webId ? await generateEnhancedJWT(tokenData, webId) : idToken,
podAccess: !!webId
};
return authResult;
}
interface WebIdMetrics {
totalCreations: number;
successfulCreations: number;
failedCreations: number;
averageCreationTime: number;
}
async function logWebIdCreationFailure(
anonymousUserId: string,
error: Error
): Promise<void> {
const logEntry = {
timestamp: new Date(),
anonymousUserId: anonymousUserId,
errorType: error.name,
errorMessage: error.message,
stackTrace: error.stack,
correlationId: getCurrentCorrelationId()
};
await errorLogger.log(logEntry);
// Optional: Send alerts for high failure rates
await checkAndAlertOnFailureRates();
}
describe('WebID Creation Integration', () => {
test('should create WebID for new anonymous user', async () => {
const mockUserId = 'anon_user_123';
const expectedWebId = 'https://pod.example.com/webid/123';
// Mock Pod Platform API response
mockPodPlatformAPI.createWebId.mockResolvedValue({
uri: expectedWebId
});
const result = await createWebIdForAnonymousUser(mockUserId);
expect(result).toBe(expectedWebId);
expect(mockPodPlatformAPI.createWebId).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': expect.stringContaining('Bearer')
})
})
);
});
test('should return existing WebID for returning user', async () => {
const mockUserId = 'anon_user_456';
const existingWebId = 'https://pod.example.com/webid/456';
// Setup existing mapping
await storeUserWebIdMapping(mockUserId, existingWebId);
const result = await handlePostAuthentication(createMockJWT(mockUserId));
expect(result).toBe(existingWebId);
// Should not call Pod Platform API
expect(mockPodPlatformAPI.createWebId).not.toHaveBeenCalled();
});
});
describe('Full Authentication Flow with WebID', () => {
test('should complete authentication and create WebID', async () => {
// Test full flow from authorization code to WebID creation
const authCode = 'test_auth_code';
const mockIdToken = createMockIdToken();
// Mock MS Auth middleware response
mockAuthMiddleware.exchangeCode.mockResolvedValue({
access_token: 'access_token',
id_token: mockIdToken
});
// Mock Pod Platform
mockPodPlatformAPI.createWebId.mockResolvedValue({
uri: 'https://pod.example.com/webid/test'
});
const result = await processAuthenticationFlow(authCode);
expect(result.webId).toBeDefined();
expect(result.podAccess).toBe(true);
});
});
This specification provides a comprehensive approach to integrating WebID creation into the existing VKN OIDC implementation. The enhanced system will enable anonymous users to seamlessly access the WellData ecosystem while maintaining their privacy and anonymity. The implementation prioritizes reliability, security, and user experience while providing appropriate fallback mechanisms for system resilience.