- Add Legal Office of the Master seal (SVG design with Maltese Cross, scales of justice, legal scroll) - Create legal-office-manifest-template.json for Legal Office credentials - Update SEAL_MAPPING.md and DESIGN_GUIDE.md with Legal Office seal documentation - Complete Azure CDN infrastructure deployment: - Resource group, storage account, and container created - 17 PNG seal files uploaded to Azure Blob Storage - All manifest templates updated with Azure URLs - Configuration files generated (azure-cdn-config.env) - Add comprehensive Azure CDN setup scripts and documentation - Fix manifest URL generation to prevent double slashes - Verify all seals accessible via HTTPS
255 lines
7.4 KiB
TypeScript
255 lines
7.4 KiB
TypeScript
/**
|
|
* eIDAS to Microsoft Entra VerifiedID Bridge
|
|
* Connects eIDAS verification to Microsoft Entra VerifiedID for credential issuance
|
|
*/
|
|
|
|
import { EIDASProvider, EIDASSignature } from './eidas';
|
|
import { EntraVerifiedIDClient, VerifiableCredentialRequest, ClaimValue } from './entra-verifiedid';
|
|
import { AzureLogicAppsClient } from './azure-logic-apps';
|
|
import { validateBase64File, FILE_SIZE_LIMITS, encodeFileToBase64, FileValidationOptions } from './file-utils';
|
|
|
|
export interface EIDASToEntraConfig {
|
|
entraVerifiedID: {
|
|
tenantId: string;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
credentialManifestId: string;
|
|
};
|
|
eidas: {
|
|
providerUrl: string;
|
|
apiKey: string;
|
|
};
|
|
logicApps?: {
|
|
workflowUrl: string;
|
|
accessKey?: string;
|
|
managedIdentityClientId?: string;
|
|
};
|
|
}
|
|
|
|
export interface EIDASVerificationResult {
|
|
verified: boolean;
|
|
eidasSignature?: EIDASSignature;
|
|
certificateChain?: string[];
|
|
subject?: string;
|
|
issuer?: string;
|
|
validityPeriod?: {
|
|
notBefore: Date;
|
|
notAfter: Date;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Bridge between eIDAS verification and Microsoft Entra VerifiedID issuance
|
|
*/
|
|
export class EIDASToEntraBridge {
|
|
private eidasProvider: EIDASProvider;
|
|
private entraClient: EntraVerifiedIDClient;
|
|
private logicAppsClient?: AzureLogicAppsClient;
|
|
|
|
constructor(config: EIDASToEntraConfig) {
|
|
this.eidasProvider = new EIDASProvider({
|
|
providerUrl: config.eidas.providerUrl,
|
|
apiKey: config.eidas.apiKey,
|
|
});
|
|
|
|
this.entraClient = new EntraVerifiedIDClient({
|
|
tenantId: config.entraVerifiedID.tenantId,
|
|
clientId: config.entraVerifiedID.clientId,
|
|
clientSecret: config.entraVerifiedID.clientSecret,
|
|
credentialManifestId: config.entraVerifiedID.credentialManifestId,
|
|
});
|
|
|
|
if (config.logicApps) {
|
|
this.logicAppsClient = new AzureLogicAppsClient(config.logicApps);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify eIDAS signature and issue credential via Entra VerifiedID
|
|
*/
|
|
async verifyAndIssue(
|
|
document: string | Buffer,
|
|
userId: string,
|
|
userEmail: string,
|
|
pin?: string,
|
|
validationOptions?: FileValidationOptions
|
|
): Promise<{
|
|
verified: boolean;
|
|
credentialRequest?: {
|
|
requestId: string;
|
|
url: string;
|
|
qrCode?: string;
|
|
};
|
|
errors?: string[];
|
|
}> {
|
|
// Step 0: Validate and encode document if needed
|
|
let documentBase64: string;
|
|
|
|
if (document instanceof Buffer) {
|
|
// Encode buffer to base64
|
|
documentBase64 = encodeFileToBase64(document);
|
|
} else {
|
|
// Validate base64 string
|
|
const validation = validateBase64File(
|
|
document as string,
|
|
validationOptions || {
|
|
maxSize: FILE_SIZE_LIMITS.MEDIUM,
|
|
allowedMimeTypes: [
|
|
'application/pdf',
|
|
'image/png',
|
|
'image/jpeg',
|
|
'application/json',
|
|
'text/plain',
|
|
],
|
|
}
|
|
);
|
|
|
|
if (!validation.valid) {
|
|
return {
|
|
verified: false,
|
|
errors: validation.errors,
|
|
};
|
|
}
|
|
|
|
documentBase64 = document as string;
|
|
}
|
|
|
|
// Step 1: Request eIDAS signature
|
|
let eidasSignature: EIDASSignature;
|
|
try {
|
|
eidasSignature = await this.eidasProvider.requestSignature(documentBase64);
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
console.error('eIDAS signature request failed:', errorMessage);
|
|
return {
|
|
verified: false,
|
|
errors: [`eIDAS signature request failed: ${errorMessage}`],
|
|
};
|
|
}
|
|
|
|
// Step 2: Verify eIDAS signature
|
|
const verified = await this.eidasProvider.verifySignature(eidasSignature);
|
|
if (!verified) {
|
|
return {
|
|
verified: false,
|
|
errors: ['eIDAS signature verification failed'],
|
|
};
|
|
}
|
|
|
|
// Step 3: Trigger Logic App workflow if configured
|
|
if (this.logicAppsClient) {
|
|
try {
|
|
const documentId = document instanceof Buffer ? document.toString('base64').substring(0, 100) : (document as string).substring(0, 100);
|
|
await this.logicAppsClient.triggerEIDASVerification(
|
|
documentId,
|
|
userId,
|
|
this.eidasProvider['config'].providerUrl
|
|
);
|
|
} catch (error) {
|
|
console.warn('Logic App trigger failed (non-blocking):', error);
|
|
}
|
|
}
|
|
|
|
// Step 4: Issue credential via Entra VerifiedID
|
|
const credentialRequest: VerifiableCredentialRequest = {
|
|
claims: {
|
|
email: userEmail,
|
|
userId,
|
|
eidasVerified: true, // Boolean value (will be converted to string)
|
|
eidasCertificate: eidasSignature.certificate,
|
|
eidasSignatureTimestamp: eidasSignature.timestamp.toISOString(),
|
|
},
|
|
pin,
|
|
};
|
|
|
|
try {
|
|
const credentialResponse = await this.entraClient.issueCredential(credentialRequest);
|
|
|
|
return {
|
|
verified: true,
|
|
credentialRequest: {
|
|
requestId: credentialResponse.requestId,
|
|
url: credentialResponse.url,
|
|
qrCode: credentialResponse.qrCode,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('Entra VerifiedID credential issuance failed:', error);
|
|
return { verified: true }; // eIDAS verified but credential issuance failed
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify eIDAS signature only (without issuing credential)
|
|
*/
|
|
async verifyEIDAS(document: string): Promise<EIDASVerificationResult> {
|
|
try {
|
|
const signature = await this.eidasProvider.requestSignature(document);
|
|
const verified = await this.eidasProvider.verifySignature(signature);
|
|
|
|
if (!verified) {
|
|
return { verified: false };
|
|
}
|
|
|
|
// Extract certificate information (simplified - in production parse certificate)
|
|
return {
|
|
verified: true,
|
|
eidasSignature: signature,
|
|
subject: 'eIDAS Subject', // Would be extracted from certificate
|
|
issuer: 'eIDAS Issuer', // Would be extracted from certificate
|
|
validityPeriod: {
|
|
notBefore: signature.timestamp,
|
|
notAfter: new Date(signature.timestamp.getTime() + 365 * 24 * 60 * 60 * 1000), // 1 year default
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('eIDAS verification failed:', error);
|
|
return { verified: false };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Issue credential based on verified eIDAS signature
|
|
*/
|
|
async issueCredentialFromEIDAS(
|
|
eidasVerificationResult: EIDASVerificationResult,
|
|
userId: string,
|
|
userEmail: string,
|
|
additionalClaims?: Record<string, ClaimValue>,
|
|
pin?: string
|
|
): Promise<{
|
|
requestId: string;
|
|
url: string;
|
|
qrCode?: string;
|
|
}> {
|
|
if (!eidasVerificationResult.verified || !eidasVerificationResult.eidasSignature) {
|
|
throw new Error('eIDAS verification must be successful before issuing credential');
|
|
}
|
|
|
|
const claims: Record<string, ClaimValue> = {
|
|
email: userEmail,
|
|
userId,
|
|
eidasVerified: true, // Boolean value (will be converted to string)
|
|
eidasCertificate: eidasVerificationResult.eidasSignature.certificate,
|
|
eidasSignatureTimestamp: eidasVerificationResult.eidasSignature.timestamp.toISOString(),
|
|
...additionalClaims,
|
|
};
|
|
|
|
if (eidasVerificationResult.subject) {
|
|
claims.eidasSubject = eidasVerificationResult.subject;
|
|
}
|
|
|
|
if (eidasVerificationResult.issuer) {
|
|
claims.eidasIssuer = eidasVerificationResult.issuer;
|
|
}
|
|
|
|
const credentialRequest: VerifiableCredentialRequest = {
|
|
claims,
|
|
pin,
|
|
};
|
|
|
|
return await this.entraClient.issueCredential(credentialRequest);
|
|
}
|
|
}
|
|
|