1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
|
import dotenv from 'dotenv';
import express, { Request, Response } from 'express';
import { randomBytes } from 'crypto';
import qrcode from 'qrcode';
// TODO: Import digiidTs library once linked
// import * as digiidTs from 'digiid-ts';
// Load environment variables from .env file
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3001; // Default to 3001 if PORT is not set
// In-memory storage for demo purposes
// NOTE: In a production application, use a persistent store (e.g., Redis, database)
interface SessionState {
nonce: string;
status: 'pending' | 'success' | 'failed';
address?: string;
error?: string;
}
const sessionStore = new Map<string, SessionState>();
const nonceToSessionMap = new Map<string, string>();
// Middleware
app.use(express.json()); // Parse JSON bodies
console.log('Server starting...');
console.log(`Attempting to listen on port: ${PORT}`);
console.log(`Configured PUBLIC_URL: ${process.env.PUBLIC_URL}`);
// TODO: Implement DigiID API endpoints
// Endpoint to initiate the DigiID authentication flow
app.get('/api/digiid/start', async (req: Request, res: Response) => {
try {
const sessionId = randomBytes(16).toString('hex');
const nonce = randomBytes(16).toString('hex');
const publicUrl = process.env.PUBLIC_URL;
if (!publicUrl) {
console.error('PUBLIC_URL environment variable is not set.');
return res.status(500).json({ error: 'Server configuration error: PUBLIC_URL is missing.' });
}
let callbackUrl: string;
try {
const baseUrl = new URL(publicUrl);
callbackUrl = new URL('/api/digiid/callback', baseUrl).toString();
} catch (error) {
console.error('Invalid PUBLIC_URL format:', publicUrl, error);
return res.status(500).json({ error: 'Server configuration error: Invalid PUBLIC_URL format.' });
}
// Determine if the callback URL is insecure (HTTP)
const unsecure = callbackUrl.startsWith('http://');
// Placeholder for digiidTs.generateDigiIDUri call
// const digiIdUri = digiidTs.generateDigiIDUri({ callbackUrl, unsecure, nonce });
const digiIdUri = `digiid://example.com?x=${nonce}&unsecure=${unsecure ? 1 : 0}&callback=${encodeURIComponent(callbackUrl)}`; // TEMPORARY PLACEHOLDER
console.log(`Generated DigiID URI: ${digiIdUri} for session ${sessionId}`);
// Store session state
sessionStore.set(sessionId, { nonce, status: 'pending' });
nonceToSessionMap.set(nonce, sessionId);
console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`);
// Generate QR code
const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri);
res.json({ sessionId, qrCodeDataUrl });
} catch (error) {
console.error('Error in /api/digiid/start:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Callback endpoint for the DigiID mobile app
app.post('/api/digiid/callback', async (req: Request, res: Response) => {
const { address, uri, signature } = req.body;
console.log('Received callback:', { address, uri, signature });
if (!address || !uri || !signature) {
console.warn('Callback missing required fields.');
// Note: DigiID protocol doesn't expect a body response on failure here,
// just a non-200 status, but sending JSON for easier debugging.
return res.status(400).json({ error: 'Missing required callback parameters.' });
}
let receivedNonce: string | null = null;
try {
// Parse the nonce (parameter 'x') from the received URI
const parsedUri = new URL(uri);
receivedNonce = parsedUri.searchParams.get('x');
} catch (error) {
console.warn('Error parsing received URI:', uri, error);
return res.status(400).json({ error: 'Invalid URI format.' });
}
if (!receivedNonce) {
console.warn('Nonce (x parameter) not found in received URI:', uri);
return res.status(400).json({ error: 'Nonce not found in URI.' });
}
// Find the session ID associated with the nonce
const sessionId = nonceToSessionMap.get(receivedNonce);
if (!sessionId) {
console.warn('Received nonce does not correspond to any active session:', receivedNonce);
// This could happen if the session expired or the nonce is invalid/reused
return res.status(404).json({ error: 'Session not found or expired for this nonce.' });
}
// Retrieve the session state
const session = sessionStore.get(sessionId);
if (!session || session.status !== 'pending') {
console.warn('Session not found or not in pending state for ID:', sessionId);
// Should ideally not happen if nonceToSessionMap is consistent with sessionStore
return res.status(404).json({ error: 'Session not found or already completed/failed.' });
}
// Construct the expected callback URL (must match the one used in /start)
const publicUrl = process.env.PUBLIC_URL;
if (!publicUrl) {
// This should have been caught in /start, but double-check
console.error('PUBLIC_URL is unexpectedly missing during callback.');
session.status = 'failed';
session.error = 'Server configuration error: PUBLIC_URL missing.';
return res.status(500).send(); // Send 500 internal server error
}
let expectedCallbackUrl: string;
try {
expectedCallbackUrl = new URL('/api/digiid/callback', publicUrl).toString();
} catch (error) {
console.error('Invalid PUBLIC_URL format during callback:', publicUrl, error);
session.status = 'failed';
session.error = 'Server configuration error: Invalid PUBLIC_URL.';
return res.status(500).send();
}
const expectedNonce = session.nonce;
try {
console.log('Verifying callback with:', {
address,
uri,
signature,
expectedCallbackUrl,
expectedNonce,
});
// Placeholder for digiidTs.verifyDigiIDCallback call
// const isValid = await digiidTs.verifyDigiIDCallback({
// address,
// uri,
// signature,
// callbackUrl: expectedCallbackUrl,
// nonce: expectedNonce,
// });
// --- TEMPORARY PLACEHOLDER VERIFICATION ---
// Simulating verification: check if nonce matches and URI contains expected callback
const isValid = receivedNonce === expectedNonce && uri.includes(expectedCallbackUrl);
console.log(`Placeholder verification result: ${isValid}`);
// --- END PLACEHOLDER ---
if (isValid) {
console.log(`Verification successful for session ${sessionId}, address: ${address}`);
session.status = 'success';
session.address = address;
// Clean up nonce lookup map once verified successfully
nonceToSessionMap.delete(expectedNonce);
} else {
console.warn(`Verification failed for session ${sessionId}`);
session.status = 'failed';
session.error = 'Signature verification failed.';
// Keep nonce in map for potential debugging, or clean up based on policy
// nonceToSessionMap.delete(expectedNonce);
}
// Update the session store
sessionStore.set(sessionId, session);
// DigiID protocol expects a 200 OK on success/failure after processing
res.status(200).send();
} catch (error) {
console.error('Error during callback verification process for session:', sessionId, error);
// Update session state to reflect the error
session.status = 'failed';
session.error = 'Internal server error during verification.';
sessionStore.set(sessionId, session);
// Don't expose internal errors via status code if possible, but log them
res.status(200).send(); // Still send 200 as processing happened, but log indicates issue
}
});
// Endpoint to check the status of an authentication session
app.get('/api/digiid/status/:sessionId', (req: Request, res: Response) => {
const { sessionId } = req.params;
const session = sessionStore.get(sessionId);
if (!session) {
return res.status(404).json({ status: 'not_found' });
}
// Return only the necessary fields to the client
const { status, address, error } = session;
res.json({ status, address, error }); // address and error will be undefined if not set
});
app.get('/', (_: Request, res: Response) => {
res.send('DigiID Demo Backend Running!');
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
|