summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/App.tsx103
-rw-r--r--src/client/main.tsx2
-rw-r--r--src/client/utils.ts19
-rw-r--r--src/server/main.ts316
-rw-r--r--src/server/utils.ts10
5 files changed, 280 insertions, 170 deletions
diff --git a/src/client/App.tsx b/src/client/App.tsx
index b3f0308..ad1e230 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -7,7 +7,7 @@ type UiState = 'initial' | 'waiting' | 'success' | 'failed';
// Define the structure for result data (success or failure)
interface ResultData {
address?: string; // Present on success
- error?: string; // Present on failure
+ error?: string; // Present on failure
addressType?: string; // Added later
}
@@ -30,7 +30,7 @@ function App() {
}
console.log(`Starting status polling for session: ${sessionId}`);
- let intervalId: any = null; // Use 'any' to avoid browser/node type conflict for setInterval return type
+ let intervalId: ReturnType<typeof setInterval> | null = null;
const checkStatus = async () => {
if (!sessionId) return; // Should not happen here, but type guard
@@ -41,19 +41,26 @@ function App() {
if (!response.ok) {
// Handle specific errors like 404 (session not found/expired)
if (response.status === 404) {
- console.warn(`Session ${sessionId} not found or expired during polling.`);
+ console.warn(
+ `Session ${sessionId} not found or expired during polling.`
+ );
setError('Session expired or could not be found.');
setUiState('failed'); // Transition to failed state
setResultData({ error: 'Session expired or could not be found.' });
} else {
- const errorData = await response.json().catch(() => ({ message: 'Error fetching status' }));
- throw new Error(errorData.message || `Server responded with ${response.status}`);
+ const errorData = await response
+ .json()
+ .catch(() => ({ message: 'Error fetching status' }));
+ throw new Error(
+ errorData.message || `Server responded with ${response.status}`
+ );
}
if (intervalId) clearInterval(intervalId); // Stop polling on error
return;
}
- const data: { status: UiState, address?: string, error?: string } = await response.json();
+ const data: { status: UiState; address?: string; error?: string } =
+ await response.json();
console.log('Received status data:', data);
// If status changed from pending, update UI and stop polling
@@ -67,10 +74,12 @@ function App() {
if (intervalId) clearInterval(intervalId);
}
// If status is still 'pending', the interval will continue
-
} catch (err) {
console.error('Error polling status:', err);
- const message = err instanceof Error ? err.message : 'An unknown error occurred during status check';
+ const message =
+ err instanceof Error
+ ? err.message
+ : 'An unknown error occurred during status check';
setError(`Status polling failed: ${message}`);
// Decide if we should stop polling or transition state on generic fetch error
// For now, let's stop polling and show error, moving to failed state
@@ -104,7 +113,11 @@ function App() {
try {
const response = await fetch('/api/digiid/start');
if (!response.ok) {
- const errorData = await response.json().catch(() => ({ message: 'Failed to start session. Server responded with status: ' + response.status }));
+ const errorData = await response.json().catch(() => ({
+ message:
+ 'Failed to start session. Server responded with status: ' +
+ response.status,
+ }));
throw new Error(errorData.message || 'Failed to start session');
}
const data = await response.json();
@@ -114,7 +127,8 @@ function App() {
setUiState('waiting'); // Move to waiting state
} catch (err) {
console.error('Error starting Digi-ID session:', err);
- const message = err instanceof Error ? err.message : 'An unknown error occurred';
+ const message =
+ err instanceof Error ? err.message : 'An unknown error occurred';
setError(`Failed to initiate Digi-ID: ${message}`);
setUiState('initial'); // Stay in initial state on error
} finally {
@@ -140,9 +154,20 @@ function App() {
{/* --- Views based on uiState --- */}
{uiState === 'initial' && (
<div className="initial-view">
- <button onClick={handleStart} disabled={isLoading} className="signin-button">
- <img src="/assets/digiid-logo.png" alt="Digi-ID Logo" width="24" height="24" />
- <span>{isLoading ? 'Generating QR...' : 'Sign in with Digi-ID'}</span>
+ <button
+ onClick={handleStart}
+ disabled={isLoading}
+ className="signin-button"
+ >
+ <img
+ src="/assets/digiid-logo.png"
+ alt="Digi-ID Logo"
+ width="24"
+ height="24"
+ />
+ <span>
+ {isLoading ? 'Generating QR...' : 'Sign in with Digi-ID'}
+ </span>
</button>
</div>
)}
@@ -152,7 +177,10 @@ function App() {
{uiState === 'waiting' && qrCodeDataUrl && (
<div className="waiting-view view-box">
<h2>Scan the QR Code</h2>
- <p>Scan the QR code below using your Digi-ID compatible mobile wallet.</p>
+ <p>
+ Scan the QR code below using your Digi-ID compatible mobile
+ wallet.
+ </p>
<img src={qrCodeDataUrl} alt="Digi-ID QR Code" width="250" />
<p>Waiting for authentication...</p>
<button onClick={handleReset}>Cancel</button>
@@ -161,14 +189,30 @@ function App() {
{uiState === 'success' && resultData?.address && (
<div className="success-view view-box">
- <svg xmlns="http://www.w3.org/2000/svg" className="checkmark-icon" viewBox="0 0 52 52">
- <circle className="checkmark__circle" cx="26" cy="26" r="25" fill="none"/>
- <path className="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ className="checkmark-icon"
+ viewBox="0 0 52 52"
+ >
+ <circle
+ className="checkmark__circle"
+ cx="26"
+ cy="26"
+ r="25"
+ fill="none"
+ />
+ <path
+ className="checkmark__check"
+ fill="none"
+ d="M14.1 27.2l7.1 7.2 16.7-16.8"
+ />
</svg>
<h2>Authentication Successful!</h2>
<p>Verified Address:</p>
<p className="address">{resultData.address}</p>
- <p>Address Format: {getDigiByteAddressType(resultData.address)}</p>
+ <p>
+ Address Format: {getDigiByteAddressType(resultData.address)}
+ </p>
<button onClick={handleReset}>Start Over</button>
</div>
)}
@@ -177,7 +221,8 @@ function App() {
<div className="failed-view view-box">
<h2>Authentication Failed</h2>
<p className="error-message">
- Reason: {resultData?.error || error || 'An unknown error occurred.'}
+ Reason:{' '}
+ {resultData?.error || error || 'An unknown error occurred.'}
</p>
<button onClick={handleReset}>Try Again</button>
</div>
@@ -188,14 +233,26 @@ function App() {
{/* --- Description section (Always visible below the view container) --- */}
<div className="description">
<p className="code-link">
- This application demonstrates integrating Digi-ID authentication using the <a href="https://github.com/pawelzelawski/digiid-ts" target="_blank" rel="noopener noreferrer">digiid-ts</a> library.
+ This application demonstrates integrating Digi-ID authentication using
+ the{' '}
+ <a
+ href="https://github.com/pawelzelawski/digiid-ts"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ digiid-ts
+ </a>{' '}
+ library.
</p>
<p>
- Upon successful verification, the system can identify the following DigiByte address formats:
+ Upon successful verification, the system can identify the following
+ DigiByte address formats:
</p>
<ul>
<li>Legacy Addresses (P2PKH) - starting with 'D'</li>
- <li>Pay-to-Script-Hash Addresses (P2SH) - commonly starting with 'S'</li>
+ <li>
+ Pay-to-Script-Hash Addresses (P2SH) - commonly starting with 'S'
+ </li>
<li>Segregated Witness (SegWit) Addresses - starting with 'dgb1'</li>
</ul>
</div>
@@ -203,4 +260,4 @@ function App() {
);
}
-export default App; \ No newline at end of file
+export default App;
diff --git a/src/client/main.tsx b/src/client/main.tsx
index b895c80..0896e5e 100644
--- a/src/client/main.tsx
+++ b/src/client/main.tsx
@@ -7,4 +7,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
-); \ No newline at end of file
+);
diff --git a/src/client/utils.ts b/src/client/utils.ts
index 6a95f7b..1677749 100644
--- a/src/client/utils.ts
+++ b/src/client/utils.ts
@@ -1,6 +1,10 @@
// Utility to determine DigiByte address type based on prefix
-export type DigiByteAddressFormat = 'Legacy (P2PKH)' | 'Script (P2SH)' | 'SegWit (Bech32)' | 'Unknown';
+export type DigiByteAddressFormat =
+ | 'Legacy (P2PKH)'
+ | 'Script (P2SH)'
+ | 'SegWit (Bech32)'
+ | 'Unknown';
/**
* Determines the format of a DigiByte address based on its prefix.
@@ -11,21 +15,24 @@ export type DigiByteAddressFormat = 'Legacy (P2PKH)' | 'Script (P2SH)' | 'SegWit
* @param address The DigiByte address string.
* @returns The determined address format.
*/
-export function getDigiByteAddressType(address: string | undefined | null): DigiByteAddressFormat {
+export function getDigiByteAddressType(
+ address: string | undefined | null
+): DigiByteAddressFormat {
if (!address) {
return 'Unknown';
}
if (address.startsWith('dgb1')) {
return 'SegWit (Bech32)';
}
- if (address.startsWith('S')) { // Common prefix for P2SH on DGB
- return 'Script (P2SH)';
+ if (address.startsWith('S')) {
+ // Common prefix for P2SH on DGB
+ return 'Script (P2SH)';
}
if (address.startsWith('D')) {
- return 'Legacy (P2PKH)';
+ return 'Legacy (P2PKH)';
}
// If it doesn't match known prefixes, return Unknown
// Could potentially add DigiAsset checks here if they have distinct prefixes
return 'Unknown';
-} \ No newline at end of file
+}
diff --git a/src/server/main.ts b/src/server/main.ts
index 96afb0e..1ab9004 100644
--- a/src/server/main.ts
+++ b/src/server/main.ts
@@ -28,157 +28,199 @@ console.log(`Attempting to listen on port: ${PORT}`);
console.log(`Configured PUBLIC_URL: ${process.env.PUBLIC_URL}`);
// Endpoint to initiate the DigiID authentication flow
-app.get('/api/digiid/start', (req: Request, res: Response, next: NextFunction) => {
- (async () => {
- 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;
+app.get(
+ '/api/digiid/start',
+ (req: Request, res: Response, next: NextFunction) => {
+ (async () => {
try {
- const baseUrl = new URL(publicUrl);
- callbackUrl = new URL('/api/digiid/callback', baseUrl).toString();
+ 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.',
+ });
+ }
+
+ const unsecure = callbackUrl.startsWith('http://');
+ // Use the actual function from digiid-ts
+ const digiIdUri = generateDigiIDUri({ callbackUrl, unsecure, nonce });
+ console.log(
+ `Generated DigiID URI: ${digiIdUri} for session ${sessionId}`
+ );
+
+ sessionStore.set(sessionId, { nonce, status: 'pending' });
+ nonceToSessionMap.set(nonce, sessionId);
+ console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`);
+
+ const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri);
+ res.json({ sessionId, qrCodeDataUrl });
} catch (error) {
- console.error('Invalid PUBLIC_URL format:', publicUrl, error);
- return res.status(500).json({ error: 'Server configuration error: Invalid PUBLIC_URL format.' });
+ console.error('Error in /api/digiid/start:', error);
+ // Ensure response is sent even on error
+ if (!res.headersSent) {
+ // Check if response hasn't already been sent
+ if (error instanceof Error) {
+ res
+ .status(400)
+ .json({ error: `Failed to generate URI: ${error.message}` });
+ } else {
+ res
+ .status(500)
+ .json({ error: 'Internal server error during start' });
+ }
+ }
}
+ })().catch(next);
+ }
+);
- const unsecure = callbackUrl.startsWith('http://');
- // Use the actual function from digiid-ts
- const digiIdUri = generateDigiIDUri({ callbackUrl, unsecure, nonce });
- console.log(`Generated DigiID URI: ${digiIdUri} for session ${sessionId}`);
+// Callback endpoint for the DigiID mobile app
+app.post(
+ '/api/digiid/callback',
+ (req: Request, res: Response, next: NextFunction) => {
+ (async () => {
+ const { address, uri, signature } = req.body;
+
+ // Basic validation of received data
+ if (!address || !uri || !signature) {
+ console.warn('Callback missing required fields.', {
+ address,
+ uri,
+ signature,
+ });
+ // Wallet doesn't expect a body on failure, just non-200. Status only for logging/debug.
+ return res.status(400).send('Missing required callback parameters.');
+ }
- sessionStore.set(sessionId, { nonce, status: 'pending' });
- nonceToSessionMap.set(nonce, sessionId);
- console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`);
+ const callbackData = { address, uri, signature };
+ console.log('Received callback:', callbackData);
- const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri);
- res.json({ sessionId, qrCodeDataUrl });
+ // --- Nonce Extraction and Session Lookup ---
+ let receivedNonce: string | null = null;
+ try {
+ // DigiID URIs need scheme replaced for standard URL parsing
+ const parsableUri = uri.replace(/^digiid:/, 'http:');
+ const parsedUri = new URL(parsableUri);
+ receivedNonce = parsedUri.searchParams.get('x');
+ } catch (error) {
+ console.warn('Error parsing received URI:', uri, error);
+ return res.status(400).send('Invalid URI format.');
+ }
- } catch (error) {
- console.error('Error in /api/digiid/start:', error);
- // Ensure response is sent even on error
- if (!res.headersSent) { // Check if response hasn't already been sent
- if (error instanceof Error) {
- res.status(400).json({ error: `Failed to generate URI: ${error.message}` });
- } else {
- res.status(500).json({ error: 'Internal server error during start' });
- }
+ if (!receivedNonce) {
+ console.warn('Nonce (x parameter) not found in received URI:', uri);
+ return res.status(400).send('Nonce not found in URI.');
}
- }
- })().catch(next);
-});
-// Callback endpoint for the DigiID mobile app
-app.post('/api/digiid/callback', (req: Request, res: Response, next: NextFunction) => {
- (async () => {
- const { address, uri, signature } = req.body;
-
- // Basic validation of received data
- if (!address || !uri || !signature) {
- console.warn('Callback missing required fields.', { address, uri, signature });
- // Wallet doesn't expect a body on failure, just non-200. Status only for logging/debug.
- return res.status(400).send('Missing required callback parameters.');
- }
-
- const callbackData = { address, uri, signature };
- console.log('Received callback:', callbackData);
-
- // --- Nonce Extraction and Session Lookup ---
- let receivedNonce: string | null = null;
- try {
- // DigiID URIs need scheme replaced for standard URL parsing
- const parsableUri = uri.replace(/^digiid:/, 'http:');
- const parsedUri = new URL(parsableUri);
- receivedNonce = parsedUri.searchParams.get('x');
- } catch (error) {
- console.warn('Error parsing received URI:', uri, error);
- return res.status(400).send('Invalid URI format.');
- }
-
- if (!receivedNonce) {
- console.warn('Nonce (x parameter) not found in received URI:', uri);
- return res.status(400).send('Nonce not found in URI.');
- }
-
- const sessionId = nonceToSessionMap.get(receivedNonce);
- if (!sessionId) {
- console.warn('Session not found for received nonce:', receivedNonce);
- // Nonce might be expired or invalid
- return res.status(404).send('Session not found or expired for this nonce.');
- }
-
- // Retrieve the session *before* the try/finally block for verification
- const session = sessionStore.get(sessionId);
- if (!session) {
- console.error(`Critical: Session data missing for ${sessionId} despite nonce match.`);
- if (!res.headersSent) res.status(500).send('Internal server error: Session data missing.');
+ const sessionId = nonceToSessionMap.get(receivedNonce);
+ if (!sessionId) {
+ console.warn('Session not found for received nonce:', receivedNonce);
+ // Nonce might be expired or invalid
+ return res
+ .status(404)
+ .send('Session not found or expired for this nonce.');
+ }
+
+ // Retrieve the session *before* the try/finally block for verification
+ const session = sessionStore.get(sessionId);
+ if (!session) {
+ console.error(
+ `Critical: Session data missing for ${sessionId} despite nonce match.`
+ );
+ if (!res.headersSent)
+ res.status(500).send('Internal server error: Session data missing.');
return; // Explicitly return void
- }
- if (session.status !== 'pending') {
+ }
+ if (session.status !== 'pending') {
console.warn('Session already processed:', sessionId, session.status);
- if (!res.headersSent) res.status(200).send('Session already processed.');
+ if (!res.headersSent)
+ res.status(200).send('Session already processed.');
+ return; // Explicitly return void
+ }
+
+ // --- Verification ---
+ let expectedCallbackUrl: string;
+ try {
+ const publicUrl = process.env.PUBLIC_URL;
+ if (!publicUrl)
+ throw new Error(
+ 'PUBLIC_URL environment variable is not configured on the server.'
+ );
+ expectedCallbackUrl = new URL(
+ '/api/digiid/callback',
+ publicUrl
+ ).toString();
+ } catch (error) {
+ console.error(
+ 'Server configuration error constructing expected callback URL:',
+ error
+ );
+ session.status = 'failed';
+ session.error = 'Server configuration error preventing verification.';
+ sessionStore.set(sessionId, session);
+ // Respond 200 OK as per protocol
+ if (!res.headersSent) res.status(200).send();
return; // Explicitly return void
- }
-
- // --- Verification ---
- let expectedCallbackUrl: string;
- try {
- const publicUrl = process.env.PUBLIC_URL;
- if (!publicUrl) throw new Error('PUBLIC_URL environment variable is not configured on the server.');
- expectedCallbackUrl = new URL('/api/digiid/callback', publicUrl).toString();
- } catch (error) {
- console.error('Server configuration error constructing expected callback URL:', error);
- session.status = 'failed';
- session.error = 'Server configuration error preventing verification.';
- sessionStore.set(sessionId, session);
- // Respond 200 OK as per protocol
- if (!res.headersSent) res.status(200).send();
- return; // Explicitly return void
- }
-
- const verifyOptions = { expectedCallbackUrl, expectedNonce: session.nonce };
-
- try {
- console.log('Attempting to verify callback with:', { callbackData, verifyOptions });
- await verifyDigiIDCallback(callbackData, verifyOptions);
-
- // Success case (no throw from verifyDigiIDCallback)
- console.log(`Verification successful for session ${sessionId}, address: ${address}`);
- session.status = 'success';
- session.address = address; // Store the verified address
- session.error = undefined; // Clear any previous error
- nonceToSessionMap.delete(session.nonce); // Clean up nonce map only on success
-
- } catch (error) {
- // Failure case (verifyDigiIDCallback threw an error)
- console.warn(`Verification failed for session ${sessionId}:`, error);
- session.status = 'failed';
- if (error instanceof Error) {
- session.error = error.message;
- } else {
- session.error = 'An unknown verification error occurred.';
}
- // Optionally cleanup nonce map on failure too, depending on policy
- // nonceToSessionMap.delete(session.nonce);
- } finally {
- // Update store and respond 200 OK in all cases (after try/catch)
- sessionStore.set(sessionId, session);
- console.log(`Final session state for ${sessionId}:`, session);
- // Ensure response is sent if not already done (e.g. in case of unexpected error before finally)
- if (!res.headersSent) {
+
+ const verifyOptions = {
+ expectedCallbackUrl,
+ expectedNonce: session.nonce,
+ };
+
+ try {
+ console.log('Attempting to verify callback with:', {
+ callbackData,
+ verifyOptions,
+ });
+ await verifyDigiIDCallback(callbackData, verifyOptions);
+
+ // Success case (no throw from verifyDigiIDCallback)
+ console.log(
+ `Verification successful for session ${sessionId}, address: ${address}`
+ );
+ session.status = 'success';
+ session.address = address; // Store the verified address
+ session.error = undefined; // Clear any previous error
+ nonceToSessionMap.delete(session.nonce); // Clean up nonce map only on success
+ } catch (error) {
+ // Failure case (verifyDigiIDCallback threw an error)
+ console.warn(`Verification failed for session ${sessionId}:`, error);
+ session.status = 'failed';
+ if (error instanceof Error) {
+ session.error = error.message;
+ } else {
+ session.error = 'An unknown verification error occurred.';
+ }
+ // Optionally cleanup nonce map on failure too, depending on policy
+ // nonceToSessionMap.delete(session.nonce);
+ } finally {
+ // Update store and respond 200 OK in all cases (after try/catch)
+ sessionStore.set(sessionId, session);
+ console.log(`Final session state for ${sessionId}:`, session);
+ // Ensure response is sent if not already done (e.g. in case of unexpected error before finally)
+ if (!res.headersSent) {
res.status(200).send();
+ }
+ // No explicit return needed here as it's the end of the function
}
- // No explicit return needed here as it's the end of the function
- }
- })().catch(next);
-});
+ })().catch(next);
+ }
+);
// Endpoint to check the status of an authentication session
app.get('/api/digiid/status/:sessionId', (req: Request, res: Response) => {
@@ -200,4 +242,4 @@ app.get('/', (_: Request, res: Response) => {
// Start the server
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
-}); \ No newline at end of file
+});
diff --git a/src/server/utils.ts b/src/server/utils.ts
index 0b1333f..bb614c0 100644
--- a/src/server/utils.ts
+++ b/src/server/utils.ts
@@ -1,6 +1,9 @@
// Simple utility to determine DigiByte address type based on prefix
-export type DigiByteAddressType = 'DigiByte (DGB)' | 'DigiAsset (DGA)' | 'Unknown';
+export type DigiByteAddressType =
+ | 'DigiByte (DGB)'
+ | 'DigiAsset (DGA)'
+ | 'Unknown';
export function getDigiByteAddressType(address: string): DigiByteAddressType {
if (address.startsWith('dgb1')) {
@@ -10,11 +13,12 @@ export function getDigiByteAddressType(address: string): DigiByteAddressType {
// For now, assume non-DGB is DigiAsset, but this might need refinement
// depending on actual DigiAsset address formats.
// If the digiid-ts library provides a helper for this, use that instead.
- else if (address) { // Basic check to differentiate from empty/null
+ else if (address) {
+ // Basic check to differentiate from empty/null
// Assuming DigiAssets might start differently or be the fallback
// This is a placeholder assumption.
// A more robust check based on DigiAsset address specification is needed.
return 'DigiAsset (DGA)'; // Placeholder - ADJUST BASED ON ACTUAL DGA PREFIX
}
return 'Unknown';
-} \ No newline at end of file
+}