docs: archive entra materials and simplify deployment docs
This commit is contained in:
+1
-2
@@ -10,7 +10,7 @@ This directory contains all backend microservices for The Order platform. Each s
|
||||
## Available Services
|
||||
|
||||
### Identity Service (`identity/`)
|
||||
- **Purpose**: Digital identity, verifiable credentials, Entra VerifiedID
|
||||
- **Purpose**: Digital identity, verifiable credentials, identity management
|
||||
- **Port**: 4002
|
||||
- **Features**: eIDAS/DID, credential issuance, identity verification
|
||||
- **Documentation**: [Identity Service README](identity/README.md)
|
||||
@@ -150,4 +150,3 @@ Services communicate via:
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-01-27
|
||||
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
/**
|
||||
* Microsoft Entra VerifiedID integration for Identity Service
|
||||
*/
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import {
|
||||
EntraVerifiedIDClient,
|
||||
VerifiableCredentialRequest,
|
||||
} from '@the-order/auth';
|
||||
import { EnhancedEntraVerifiedIDClient } from '@the-order/auth';
|
||||
import { EIDASToEntraBridge } from '@the-order/auth';
|
||||
import { getEnv } from '@the-order/shared';
|
||||
import { createVerifiableCredential } from '@the-order/database';
|
||||
import {
|
||||
entraApiRequests,
|
||||
entraApiRequestDuration,
|
||||
entraApiErrors,
|
||||
entraCredentialsIssued,
|
||||
entraIssuanceDuration,
|
||||
entraIssuanceRetries,
|
||||
entraCredentialsVerified,
|
||||
entraVerificationDuration,
|
||||
entraActiveRequests,
|
||||
} from '@the-order/monitoring';
|
||||
import { registerEntraRateLimit } from '@the-order/shared';
|
||||
|
||||
/**
|
||||
* Initialize Enhanced Entra VerifiedID client with multi-manifest support
|
||||
*/
|
||||
export function createEntraClient(): EnhancedEntraVerifiedIDClient | null {
|
||||
const env = getEnv();
|
||||
|
||||
if (!env.ENTRA_TENANT_ID || !env.ENTRA_CLIENT_ID || !env.ENTRA_CLIENT_SECRET) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse manifests from environment variable if provided
|
||||
let manifests: Record<string, string> | undefined;
|
||||
if (env.ENTRA_MANIFESTS) {
|
||||
try {
|
||||
manifests = JSON.parse(env.ENTRA_MANIFESTS);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse ENTRA_MANIFESTS, using default manifest only', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Add default manifest if provided
|
||||
if (env.ENTRA_CREDENTIAL_MANIFEST_ID) {
|
||||
manifests = manifests || {};
|
||||
manifests['default'] = env.ENTRA_CREDENTIAL_MANIFEST_ID;
|
||||
}
|
||||
|
||||
return new EnhancedEntraVerifiedIDClient({
|
||||
tenantId: env.ENTRA_TENANT_ID,
|
||||
clientId: env.ENTRA_CLIENT_ID,
|
||||
clientSecret: env.ENTRA_CLIENT_SECRET,
|
||||
credentialManifestId: env.ENTRA_CREDENTIAL_MANIFEST_ID,
|
||||
manifests,
|
||||
logoUri: env.ENTRA_CREDENTIAL_LOGO_URI,
|
||||
backgroundColor: env.ENTRA_CREDENTIAL_BG_COLOR,
|
||||
textColor: env.ENTRA_CREDENTIAL_TEXT_COLOR,
|
||||
}, {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
maxDelayMs: 10000,
|
||||
backoffMultiplier: 2,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize eIDAS to Entra bridge
|
||||
*/
|
||||
export function createEIDASToEntraBridge(): EIDASToEntraBridge | null {
|
||||
const env = getEnv();
|
||||
|
||||
if (
|
||||
!env.ENTRA_TENANT_ID ||
|
||||
!env.ENTRA_CLIENT_ID ||
|
||||
!env.ENTRA_CLIENT_SECRET ||
|
||||
!env.EIDAS_PROVIDER_URL ||
|
||||
!env.EIDAS_API_KEY
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EIDASToEntraBridge({
|
||||
entraVerifiedID: {
|
||||
tenantId: env.ENTRA_TENANT_ID,
|
||||
clientId: env.ENTRA_CLIENT_ID,
|
||||
clientSecret: env.ENTRA_CLIENT_SECRET,
|
||||
credentialManifestId: env.ENTRA_CREDENTIAL_MANIFEST_ID || '',
|
||||
},
|
||||
eidas: {
|
||||
providerUrl: env.EIDAS_PROVIDER_URL,
|
||||
apiKey: env.EIDAS_API_KEY,
|
||||
},
|
||||
logicApps: env.AZURE_LOGIC_APPS_WORKFLOW_URL
|
||||
? {
|
||||
workflowUrl: env.AZURE_LOGIC_APPS_WORKFLOW_URL,
|
||||
accessKey: env.AZURE_LOGIC_APPS_ACCESS_KEY,
|
||||
managedIdentityClientId: env.AZURE_LOGIC_APPS_MANAGED_IDENTITY_CLIENT_ID,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Entra VerifiedID routes
|
||||
*/
|
||||
export async function registerEntraRoutes(server: FastifyInstance): Promise<void> {
|
||||
const entraClient = createEntraClient();
|
||||
const eidasBridge = createEIDASToEntraBridge();
|
||||
|
||||
if (!entraClient) {
|
||||
server.log.warn('Microsoft Entra VerifiedID not configured - routes will not be available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Register Entra-specific rate limiting
|
||||
await registerEntraRateLimit(server);
|
||||
|
||||
// Issue credential via Entra VerifiedID
|
||||
server.post(
|
||||
'/vc/issue/entra',
|
||||
{
|
||||
schema: {
|
||||
description: 'Issue verifiable credential via Microsoft Entra VerifiedID',
|
||||
tags: ['credentials', 'entra'],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['claims'],
|
||||
properties: {
|
||||
claims: {
|
||||
type: 'object',
|
||||
description: 'Credential claims (values can be string, number, boolean, or null)',
|
||||
additionalProperties: {
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{ type: 'number' },
|
||||
{ type: 'boolean' },
|
||||
{ type: 'null' },
|
||||
],
|
||||
},
|
||||
},
|
||||
pin: {
|
||||
type: 'string',
|
||||
description: 'Optional PIN for credential issuance',
|
||||
},
|
||||
callbackUrl: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
description: 'Optional callback URL for issuance status',
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
qrCode: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const body = request.body as VerifiableCredentialRequest & { manifestName?: string };
|
||||
const startTime = Date.now();
|
||||
entraActiveRequests.inc({ operation: 'issueCredential' });
|
||||
|
||||
try {
|
||||
entraApiRequests.inc({ operation: 'issueCredential', status: 'attempt' });
|
||||
|
||||
const credentialResponse = await entraClient.issueCredential(body);
|
||||
|
||||
// Record success metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraIssuanceDuration.observe({ manifest_name: body.manifestName || 'default' }, duration);
|
||||
entraApiRequestDuration.observe({ operation: 'issueCredential' }, duration);
|
||||
entraCredentialsIssued.inc({ manifest_name: body.manifestName || 'default', status: 'success' });
|
||||
entraApiRequests.inc({ operation: 'issueCredential', status: 'success' });
|
||||
|
||||
return reply.status(200).send(credentialResponse);
|
||||
} catch (error) {
|
||||
// Record error metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraApiRequestDuration.observe({ operation: 'issueCredential' }, duration);
|
||||
entraApiErrors.inc({
|
||||
operation: 'issueCredential',
|
||||
error_type: error instanceof Error ? error.constructor.name : 'Unknown',
|
||||
status_code: (error as any)?.statusCode || 0,
|
||||
});
|
||||
entraCredentialsIssued.inc({ manifest_name: body.manifestName || 'default', status: 'error' });
|
||||
entraApiRequests.inc({ operation: 'issueCredential', status: 'error' });
|
||||
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to issue credential',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
entraActiveRequests.dec({ operation: 'issueCredential' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Verify credential via Entra VerifiedID
|
||||
server.post(
|
||||
'/vc/verify/entra',
|
||||
{
|
||||
schema: {
|
||||
description: 'Verify verifiable credential via Microsoft Entra VerifiedID',
|
||||
tags: ['credentials', 'entra'],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['credential'],
|
||||
properties: {
|
||||
credential: {
|
||||
type: 'object',
|
||||
description: 'Verifiable credential to verify',
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
verified: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const body = request.body as { credential: unknown };
|
||||
const startTime = Date.now();
|
||||
entraActiveRequests.inc({ operation: 'verifyCredential' });
|
||||
|
||||
try {
|
||||
entraApiRequests.inc({ operation: 'verifyCredential', status: 'attempt' });
|
||||
|
||||
const verified = await entraClient.verifyCredential(
|
||||
body.credential as Parameters<typeof entraClient.verifyCredential>[0]
|
||||
);
|
||||
|
||||
// Record success metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraVerificationDuration.observe(duration);
|
||||
entraApiRequestDuration.observe({ operation: 'verifyCredential' }, duration);
|
||||
entraCredentialsVerified.inc({ result: verified ? 'verified' : 'not_verified' });
|
||||
entraApiRequests.inc({ operation: 'verifyCredential', status: 'success' });
|
||||
|
||||
return reply.status(200).send({ verified });
|
||||
} catch (error) {
|
||||
// Record error metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraApiRequestDuration.observe({ operation: 'verifyCredential' }, duration);
|
||||
entraApiErrors.inc({
|
||||
operation: 'verifyCredential',
|
||||
error_type: error instanceof Error ? error.constructor.name : 'Unknown',
|
||||
status_code: (error as any)?.statusCode || 0,
|
||||
});
|
||||
entraCredentialsVerified.inc({ result: 'error' });
|
||||
entraApiRequests.inc({ operation: 'verifyCredential', status: 'error' });
|
||||
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to verify credential',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
entraActiveRequests.dec({ operation: 'verifyCredential' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// eIDAS verification with Entra VerifiedID issuance
|
||||
if (eidasBridge) {
|
||||
server.post(
|
||||
'/eidas/verify-and-issue',
|
||||
{
|
||||
schema: {
|
||||
description: 'Verify eIDAS signature and issue credential via Entra VerifiedID',
|
||||
tags: ['eidas', 'entra'],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['document', 'userId', 'userEmail'],
|
||||
properties: {
|
||||
document: {
|
||||
type: 'string',
|
||||
description: 'Document to verify and sign (base64-encoded string or buffer will be auto-encoded)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
description: 'User ID',
|
||||
},
|
||||
userEmail: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
description: 'User email',
|
||||
},
|
||||
pin: {
|
||||
type: 'string',
|
||||
description: 'Optional PIN for credential issuance',
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
verified: { type: 'boolean' },
|
||||
credentialRequest: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
qrCode: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const body = request.body as {
|
||||
document: string;
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
pin?: string;
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await eidasBridge.verifyAndIssue(
|
||||
body.document,
|
||||
body.userId,
|
||||
body.userEmail,
|
||||
body.pin
|
||||
);
|
||||
|
||||
if (result.verified && result.credentialRequest) {
|
||||
// Save credential request to database
|
||||
await createVerifiableCredential({
|
||||
credential_id: result.credentialRequest.requestId,
|
||||
issuer_did: `did:web:${getEnv().ENTRA_TENANT_ID}.verifiedid.msidentity.com`,
|
||||
subject_did: body.userId,
|
||||
credential_type: ['VerifiableCredential', 'EntraVerifiedIDCredential'],
|
||||
credential_subject: {
|
||||
email: body.userEmail,
|
||||
userId: body.userId,
|
||||
eidasVerified: true,
|
||||
},
|
||||
issuance_date: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(200).send(result);
|
||||
} catch (error) {
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to verify eIDAS and issue credential',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
/**
|
||||
* Microsoft Entra VerifiedID webhook/callback handler
|
||||
* Handles status updates from Entra VerifiedID service
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { getEnv } from '@the-order/shared';
|
||||
import { updateVerifiableCredential, getVerifiableCredentialById } from '@the-order/database';
|
||||
import { getEventBus, CredentialEvents } from '@the-order/events';
|
||||
import {
|
||||
entraWebhooksReceived,
|
||||
entraWebhookProcessingDuration,
|
||||
entraWebhookErrors,
|
||||
} from '@the-order/monitoring';
|
||||
import { createLogger } from '@the-order/shared';
|
||||
|
||||
const logger = createLogger('entra-webhooks');
|
||||
|
||||
export interface EntraWebhookPayload {
|
||||
requestId: string;
|
||||
requestStatus: 'request_created' | 'request_retrieved' | 'issuance_successful' | 'issuance_failed';
|
||||
state?: string;
|
||||
code?: string;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
credential?: {
|
||||
id: string;
|
||||
type: string[];
|
||||
issuer: string;
|
||||
issuanceDate: string;
|
||||
expirationDate?: string;
|
||||
credentialSubject: Record<string, unknown>;
|
||||
proof: {
|
||||
type: string;
|
||||
created: string;
|
||||
proofPurpose: string;
|
||||
verificationMethod: string;
|
||||
jws: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate webhook signature (if Entra provides one)
|
||||
* Note: Entra VerifiedID may not sign webhooks, so this is a placeholder
|
||||
*/
|
||||
function validateWebhookSignature(_payload: EntraWebhookPayload, _signature?: string): boolean {
|
||||
// TODO: Implement signature validation if Entra provides webhook signing
|
||||
// For now, we rely on HTTPS and callback URL validation
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Entra webhook payload
|
||||
*/
|
||||
async function processWebhook(payload: EntraWebhookPayload): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
const { requestId, requestStatus, error, credential } = payload;
|
||||
|
||||
try {
|
||||
logger.info('Processing Entra webhook', { requestId, requestStatus });
|
||||
|
||||
// Update metrics
|
||||
entraWebhooksReceived.inc({ event_type: requestStatus, status: 'received' });
|
||||
|
||||
// Find credential in database
|
||||
const dbCredential = await getVerifiableCredentialById(requestId);
|
||||
if (!dbCredential) {
|
||||
logger.warn('Credential not found in database', { requestId });
|
||||
entraWebhookErrors.inc({ event_type: requestStatus, error_type: 'not_found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update credential status based on webhook
|
||||
if (requestStatus === 'issuance_successful' && credential) {
|
||||
// Update credential with full credential data
|
||||
await updateVerifiableCredential(requestId, {
|
||||
status: 'issued',
|
||||
credential_data: credential,
|
||||
issued_at: new Date(credential.issuanceDate),
|
||||
expires_at: credential.expirationDate ? new Date(credential.expirationDate) : undefined,
|
||||
});
|
||||
|
||||
// Publish credential issued event
|
||||
await getEventBus().publish(CredentialEvents.ISSUED, {
|
||||
credentialId: requestId,
|
||||
issuerDid: credential.issuer,
|
||||
subjectDid: dbCredential.subject_did,
|
||||
credentialType: credential.type,
|
||||
issuedAt: credential.issuanceDate,
|
||||
});
|
||||
|
||||
logger.info('Credential issued successfully', { requestId });
|
||||
entraWebhooksReceived.inc({ event_type: requestStatus, status: 'processed' });
|
||||
} else if (requestStatus === 'issuance_failed') {
|
||||
// Update credential status to failed
|
||||
await updateVerifiableCredential(requestId, {
|
||||
status: 'failed',
|
||||
error: error?.message || 'Issuance failed',
|
||||
});
|
||||
|
||||
// Publish credential issuance failed event
|
||||
await getEventBus().publish('credential.issuance.failed', {
|
||||
credentialId: requestId,
|
||||
error: error?.message || 'Unknown error',
|
||||
});
|
||||
|
||||
logger.error('Credential issuance failed', { requestId, error });
|
||||
entraWebhookErrors.inc({ event_type: requestStatus, error_type: 'issuance_failed' });
|
||||
} else if (requestStatus === 'request_retrieved') {
|
||||
// User has retrieved the issuance request
|
||||
await updateVerifiableCredential(requestId, {
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
logger.info('Issuance request retrieved by user', { requestId });
|
||||
entraWebhooksReceived.inc({ event_type: requestStatus, status: 'processed' });
|
||||
}
|
||||
|
||||
// Record processing duration
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraWebhookProcessingDuration.observe({ event_type: requestStatus }, duration);
|
||||
} catch (error) {
|
||||
logger.error('Error processing Entra webhook', { requestId, error });
|
||||
entraWebhookErrors.inc({ event_type: requestStatus, error_type: 'processing_error' });
|
||||
entraWebhookProcessingDuration.observe({ event_type: requestStatus }, (Date.now() - startTime) / 1000);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Entra webhook routes
|
||||
*/
|
||||
export async function registerEntraWebhookRoutes(server: FastifyInstance): Promise<void> {
|
||||
const env = getEnv();
|
||||
|
||||
// Webhook endpoint for Entra VerifiedID callbacks
|
||||
server.post(
|
||||
'/vc/entra/webhook',
|
||||
{
|
||||
schema: {
|
||||
description: 'Webhook endpoint for Entra VerifiedID status updates',
|
||||
tags: ['credentials', 'entra', 'webhooks'],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['requestId', 'requestStatus'],
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
requestStatus: {
|
||||
type: 'string',
|
||||
enum: ['request_created', 'request_retrieved', 'issuance_successful', 'issuance_failed'],
|
||||
},
|
||||
state: { type: 'string' },
|
||||
code: { type: 'string' },
|
||||
error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
},
|
||||
credential: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
type: { type: 'array', items: { type: 'string' } },
|
||||
issuer: { type: 'string' },
|
||||
issuanceDate: { type: 'string' },
|
||||
expirationDate: { type: 'string' },
|
||||
credentialSubject: { type: 'object' },
|
||||
proof: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
received: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const payload = request.body as EntraWebhookPayload;
|
||||
|
||||
try {
|
||||
// Optional: Validate webhook signature if provided
|
||||
const signature = request.headers['x-entra-signature'] as string | undefined;
|
||||
if (!validateWebhookSignature(payload, signature)) {
|
||||
logger.warn('Invalid webhook signature', { requestId: payload.requestId });
|
||||
return reply.status(401).send({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Process webhook asynchronously
|
||||
processWebhook(payload).catch((error) => {
|
||||
logger.error('Async webhook processing failed', { error, requestId: payload.requestId });
|
||||
});
|
||||
|
||||
// Return immediately to acknowledge receipt
|
||||
return reply.status(200).send({ received: true });
|
||||
} catch (error) {
|
||||
logger.error('Error handling Entra webhook', { error });
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to process webhook',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Manual status check endpoint (for polling fallback)
|
||||
server.get(
|
||||
'/vc/entra/status/:requestId',
|
||||
{
|
||||
schema: {
|
||||
description: 'Check Entra credential issuance status',
|
||||
tags: ['credentials', 'entra'],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
status: { type: 'string' },
|
||||
credential: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { requestId } = request.params as { requestId: string };
|
||||
|
||||
try {
|
||||
const credential = await getVerifiableCredentialById(requestId);
|
||||
if (!credential) {
|
||||
return reply.status(404).send({ error: 'Credential not found' });
|
||||
}
|
||||
|
||||
return reply.status(200).send({
|
||||
requestId,
|
||||
status: credential.status || 'pending',
|
||||
credential: credential.credential_data || null,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error checking credential status', { error, requestId });
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to check status',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Entra webhook routes registered');
|
||||
}
|
||||
|
||||
@@ -93,14 +93,6 @@ async function initializeServer(): Promise<void> {
|
||||
// Set error handler
|
||||
server.setErrorHandler(errorHandler);
|
||||
|
||||
// Register Microsoft Entra VerifiedID routes
|
||||
const { registerEntraRoutes } = await import('./entra-integration');
|
||||
await registerEntraRoutes(server);
|
||||
|
||||
// Register Entra webhook routes
|
||||
const { registerEntraWebhookRoutes } = await import('./entra-webhooks');
|
||||
await registerEntraWebhookRoutes(server);
|
||||
|
||||
// Register batch issuance endpoint
|
||||
const { registerBatchIssuance } = await import('./batch-issuance');
|
||||
await registerBatchIssuance(server, kmsClient);
|
||||
@@ -444,4 +436,3 @@ const start = async () => {
|
||||
};
|
||||
|
||||
start();
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ export async function registerLettersOfCredenceRoutes(
|
||||
appointmentDate: { type: 'string', format: 'date-time' },
|
||||
expirationDate: { type: 'string', format: 'date-time' },
|
||||
additionalClaims: { type: 'object' },
|
||||
useEntraVerifiedID: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
description: 'Issue Letters of Credence',
|
||||
@@ -50,7 +49,6 @@ export async function registerLettersOfCredenceRoutes(
|
||||
appointmentDate: string;
|
||||
expirationDate?: string;
|
||||
additionalClaims?: Record<string, unknown>;
|
||||
useEntraVerifiedID?: boolean;
|
||||
};
|
||||
const user = (request as any).user;
|
||||
|
||||
@@ -75,8 +73,7 @@ export async function registerLettersOfCredenceRoutes(
|
||||
expirationDate: body.expirationDate ? new Date(body.expirationDate) : undefined,
|
||||
additionalClaims: body.additionalClaims,
|
||||
},
|
||||
kmsClient,
|
||||
body.useEntraVerifiedID || false
|
||||
kmsClient
|
||||
);
|
||||
|
||||
return reply.send({ credentialId });
|
||||
@@ -138,4 +135,3 @@ export async function registerLettersOfCredenceRoutes(
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* Letters of Credence issuance automation
|
||||
* Template-based generation, digital signatures, Entra VerifiedID integration, status tracking
|
||||
* Template-based generation, digital signatures, and status tracking
|
||||
*/
|
||||
|
||||
import { createVerifiableCredential } from '@the-order/database';
|
||||
import { EntraVerifiedIDClient } from '@the-order/auth';
|
||||
import { KMSClient } from '@the-order/crypto';
|
||||
import { getEnv } from '@the-order/shared';
|
||||
import { getCredentialTemplateByName, renderCredentialFromTemplate } from '@the-order/database';
|
||||
@@ -34,8 +33,7 @@ export interface LettersOfCredenceStatus {
|
||||
*/
|
||||
export async function issueLettersOfCredence(
|
||||
data: LettersOfCredenceData,
|
||||
kmsClient: KMSClient,
|
||||
useEntraVerifiedID = false
|
||||
kmsClient: KMSClient
|
||||
): Promise<string> {
|
||||
const env = getEnv();
|
||||
const issuerDid = env.VC_ISSUER_DID || (env.VC_ISSUER_DOMAIN ? `did:web:${env.VC_ISSUER_DOMAIN}` : undefined);
|
||||
@@ -74,27 +72,6 @@ export async function issueLettersOfCredence(
|
||||
|
||||
const credentialType = ['VerifiableCredential', 'DiplomaticCredential', 'LettersOfCredence'];
|
||||
|
||||
// Use Entra VerifiedID if requested and configured
|
||||
if (useEntraVerifiedID && env.ENTRA_TENANT_ID && env.ENTRA_CLIENT_ID && env.ENTRA_CLIENT_SECRET) {
|
||||
const entraClient = new EntraVerifiedIDClient({
|
||||
tenantId: env.ENTRA_TENANT_ID,
|
||||
clientId: env.ENTRA_CLIENT_ID,
|
||||
clientSecret: env.ENTRA_CLIENT_SECRET,
|
||||
credentialManifestId: env.ENTRA_CREDENTIAL_MANIFEST_ID,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const issuanceRequest = await entraClient.issueCredential({
|
||||
claims: credentialSubject as any,
|
||||
subjectDid: data.recipientDid,
|
||||
pin: undefined,
|
||||
callbackUrl: undefined,
|
||||
} as any);
|
||||
|
||||
// Store the issuance request reference
|
||||
credentialSubject.entraIssuanceRequest = issuanceRequest;
|
||||
}
|
||||
|
||||
// Sign with KMS
|
||||
const credentialData = {
|
||||
id: credentialId,
|
||||
@@ -167,4 +144,3 @@ export async function revokeLettersOfCredence(
|
||||
revocation_reason: reason,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
/**
|
||||
* Azure Logic Apps workflow integration
|
||||
* Pre-built workflows for eIDAS-Verify-And-Issue, Appointment-Credential, Batch-Renewal, Document-Attestation
|
||||
*/
|
||||
|
||||
import { AzureLogicAppsClient } from '@the-order/auth';
|
||||
import { getEnv } from '@the-order/shared';
|
||||
|
||||
export interface LogicAppsWorkflowConfig {
|
||||
eidasVerifyAndIssueUrl?: string;
|
||||
appointmentCredentialUrl?: string;
|
||||
batchRenewalUrl?: string;
|
||||
documentAttestationUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Azure Logic Apps workflows
|
||||
*/
|
||||
export function initializeLogicAppsWorkflows(
|
||||
config?: LogicAppsWorkflowConfig
|
||||
): {
|
||||
eidasVerifyAndIssue: (eidasData: unknown) => Promise<unknown>;
|
||||
appointmentCredential: (appointmentData: unknown) => Promise<unknown>;
|
||||
batchRenewal: (renewalData: unknown) => Promise<unknown>;
|
||||
documentAttestation: (documentData: unknown) => Promise<unknown>;
|
||||
} {
|
||||
const env = getEnv();
|
||||
|
||||
const eidasClient = config?.eidasVerifyAndIssueUrl
|
||||
? new AzureLogicAppsClient({
|
||||
workflowUrl: config.eidasVerifyAndIssueUrl,
|
||||
accessKey: env.AZURE_LOGIC_APPS_ACCESS_KEY,
|
||||
managedIdentityClientId: env.AZURE_LOGIC_APPS_MANAGED_IDENTITY_CLIENT_ID,
|
||||
})
|
||||
: null;
|
||||
|
||||
const appointmentClient = config?.appointmentCredentialUrl
|
||||
? new AzureLogicAppsClient({
|
||||
workflowUrl: config.appointmentCredentialUrl,
|
||||
accessKey: env.AZURE_LOGIC_APPS_ACCESS_KEY,
|
||||
managedIdentityClientId: env.AZURE_LOGIC_APPS_MANAGED_IDENTITY_CLIENT_ID,
|
||||
})
|
||||
: null;
|
||||
|
||||
const batchRenewalClient = config?.batchRenewalUrl
|
||||
? new AzureLogicAppsClient({
|
||||
workflowUrl: config.batchRenewalUrl,
|
||||
accessKey: env.AZURE_LOGIC_APPS_ACCESS_KEY,
|
||||
managedIdentityClientId: env.AZURE_LOGIC_APPS_MANAGED_IDENTITY_CLIENT_ID,
|
||||
})
|
||||
: null;
|
||||
|
||||
const documentAttestationClient = config?.documentAttestationUrl
|
||||
? new AzureLogicAppsClient({
|
||||
workflowUrl: config.documentAttestationUrl,
|
||||
accessKey: env.AZURE_LOGIC_APPS_ACCESS_KEY,
|
||||
managedIdentityClientId: env.AZURE_LOGIC_APPS_MANAGED_IDENTITY_CLIENT_ID,
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
/**
|
||||
* eIDAS Verify and Issue workflow
|
||||
* Verifies eIDAS signature and issues corresponding credential
|
||||
*/
|
||||
async eidasVerifyAndIssue(eidasData: unknown): Promise<unknown> {
|
||||
if (!eidasClient) {
|
||||
throw new Error('eIDAS Verify and Issue workflow not configured');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const eidasDataTyped = eidasData as any;
|
||||
return eidasClient.triggerEIDASVerification(
|
||||
eidasDataTyped.documentId || '',
|
||||
eidasDataTyped.userId || '',
|
||||
eidasDataTyped.eidasProviderUrl || ''
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Appointment Credential workflow
|
||||
* Issues credential based on appointment data
|
||||
*/
|
||||
async appointmentCredential(appointmentData: unknown): Promise<unknown> {
|
||||
if (!appointmentClient) {
|
||||
throw new Error('Appointment Credential workflow not configured');
|
||||
}
|
||||
|
||||
return appointmentClient.triggerWorkflow({
|
||||
body: {
|
||||
eventType: 'appointmentCredential',
|
||||
data: appointmentData,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Batch Renewal workflow
|
||||
* Renews multiple credentials in batch
|
||||
*/
|
||||
async batchRenewal(renewalData: unknown): Promise<unknown> {
|
||||
if (!batchRenewalClient) {
|
||||
throw new Error('Batch Renewal workflow not configured');
|
||||
}
|
||||
|
||||
return batchRenewalClient.triggerWorkflow({
|
||||
body: {
|
||||
eventType: 'batchRenewal',
|
||||
data: renewalData,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Document Attestation workflow
|
||||
* Attests to document authenticity and issues credential
|
||||
*/
|
||||
async documentAttestation(documentData: unknown): Promise<unknown> {
|
||||
if (!documentAttestationClient) {
|
||||
throw new Error('Document Attestation workflow not configured');
|
||||
}
|
||||
|
||||
return documentAttestationClient.triggerWorkflow({
|
||||
body: {
|
||||
eventType: 'documentAttestation',
|
||||
data: documentData,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default Logic Apps workflows
|
||||
*/
|
||||
let defaultWorkflows: ReturnType<typeof initializeLogicAppsWorkflows> | null = null;
|
||||
|
||||
export function getLogicAppsWorkflows(
|
||||
config?: LogicAppsWorkflowConfig
|
||||
): ReturnType<typeof initializeLogicAppsWorkflows> {
|
||||
if (!defaultWorkflows) {
|
||||
defaultWorkflows = initializeLogicAppsWorkflows(config);
|
||||
}
|
||||
return defaultWorkflows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user