|
| 1 | +# Getting Started with Platform Development |
| 2 | + |
| 3 | +This guide will help you get started building platforms in the metastate ecosystem. We'll cover the essential concepts and patterns you'll need to implement, using `@eCurrency-api` as a reference example. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Platforms in the metastate ecosystem follow a standard architecture pattern: |
| 8 | +1. **Authentication** - Users authenticate using their W3ID (Web3 Identity) via the `w3ds://auth` protocol |
| 9 | +2. **Webhooks** - Platform data syncs from the global eVault system via webhooks |
| 10 | +3. **Mappings** - Data transformation between global ontology and local database schemas |
| 11 | + |
| 12 | +This document focuses on authentication. For webhooks and mappings, see the other documentation files. |
| 13 | + |
| 14 | +## Authentication |
| 15 | + |
| 16 | +All platforms use a signature-based authentication system that leverages users' existing ename and keys attached to that. The authentication flow follows the `w3ds://auth` protocol. |
| 17 | + |
| 18 | +### Authentication Flow |
| 19 | + |
| 20 | +The authentication process involves these steps: |
| 21 | + |
| 22 | +1. **Client requests auth offer** → Server returns `w3ds://auth` URL with session ID |
| 23 | +2. **User signs in via w3ds client** → User is redirected back with signature |
| 24 | +3. **Server verifies signature** → Uses `signature-validator` to verify the signature |
| 25 | +4. **Server finds/creates user** → Looks up user by eName, generates JWT token |
| 26 | +5. **Client uses Bearer token** → Includes token in `Authorization: Bearer <token>` header |
| 27 | +6. **Middleware validates token** → `authMiddleware` extracts token and loads user into `req.user` |
| 28 | +7. **Protected routes** → Use `authGuard` to ensure user is authenticated |
| 29 | + |
| 30 | +### Implementation Example (eCurrency-api) |
| 31 | + |
| 32 | +#### 1. Offer Endpoint (`GET /api/auth/offer`) |
| 33 | + |
| 34 | +This endpoint generates an authentication offer URL that the client can use to initiate the login flow. |
| 35 | + |
| 36 | +```typescript |
| 37 | +getOffer = async (req: Request, res: Response) => { |
| 38 | + const baseUrl = "http://localhost:9888"; |
| 39 | + const url = new URL("/api/auth", baseUrl).toString(); |
| 40 | + const sessionId = uuidv4(); |
| 41 | + const offer = `w3ds://auth?redirect=${url}&session=${sessionId}&platform='PLATFORM NAME HERE`; |
| 42 | + res.json({ offer, sessionId }); |
| 43 | +}; |
| 44 | +``` |
| 45 | + |
| 46 | +**Response:** |
| 47 | +```json |
| 48 | +{ |
| 49 | + "offer": "w3ds://auth?redirect=http://localhost:9888/api/auth&session=abc123...&platform=ecurrency", |
| 50 | + "sessionId": "abc123..." |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +The client opens this URL in a w3ds-compatible client (like eID Wallet), which handles the user's signature and redirects back to your platform. |
| 55 | + |
| 56 | +#### 2. Login Endpoint (`POST /api/auth`) |
| 57 | + |
| 58 | +This endpoint receives the authentication result from the w3ds client and verifies the signature. |
| 59 | + |
| 60 | +**Request body:** |
| 61 | +```json |
| 62 | +{ |
| 63 | + "ename": "@user.w3id", |
| 64 | + "session": "abc123...", |
| 65 | + "w3id": "https://evault.example.com/users/123", |
| 66 | + "signature": "z..." |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +**Implementation:** |
| 71 | +```typescript |
| 72 | +login = async (req: Request, res: Response) => { |
| 73 | + const { ename, session, signature } = req.body; |
| 74 | + |
| 75 | + // Verify signature using signature-validator |
| 76 | + const verificationResult = await verifySignature({ |
| 77 | + eName: ename, |
| 78 | + signature: signature, |
| 79 | + payload: session, |
| 80 | + registryBaseUrl: process.env.PUBLIC_REGISTRY_URL, |
| 81 | + }); |
| 82 | + |
| 83 | + if (!verificationResult.valid) { |
| 84 | + return res.status(401).json({ |
| 85 | + error: "Invalid signature", |
| 86 | + message: verificationResult.error |
| 87 | + }); |
| 88 | + } |
| 89 | + |
| 90 | + // Find user by eName (users must be created via webhook first) |
| 91 | + const user = await this.userService.findUser(ename); |
| 92 | + if (!user) { |
| 93 | + return res.status(404).json({ |
| 94 | + error: "User not found", |
| 95 | + message: "User must be created via eVault webhook before authentication" |
| 96 | + }); |
| 97 | + } |
| 98 | + |
| 99 | + // Generate JWT token |
| 100 | + const token = signToken({ userId: user.id }); |
| 101 | + |
| 102 | + res.status(200).json({ |
| 103 | + user: { /* user data */ }, |
| 104 | + token, |
| 105 | + }); |
| 106 | +}; |
| 107 | +``` |
| 108 | + |
| 109 | +**Key points:** |
| 110 | +- The `session` string is what was signed by the user |
| 111 | +- Signature verification uses the `signature-validator` package, which: |
| 112 | + - Fetches the user's public key from their eVault |
| 113 | + - Verifies the signature using Web Crypto API |
| 114 | + - Supports multiple signature formats (multibase, base64, etc.) |
| 115 | +- Users must exist in your database before they can authenticate (created via webhooks) |
| 116 | +- The JWT token contains the `userId` and expires in 7 days |
| 117 | + |
| 118 | +#### 3. JWT Token Generation |
| 119 | + |
| 120 | +The JWT token is generated using a secret key stored in `JWT_SECRET` environment variable. |
| 121 | + |
| 122 | +```typescript |
| 123 | +// src/utils/jwt.ts |
| 124 | +export const signToken = (payload: AuthTokenPayload): string => { |
| 125 | + return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" }); |
| 126 | +}; |
| 127 | + |
| 128 | +export const verifyToken = (token: string): AuthTokenPayload => { |
| 129 | + const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload & AuthTokenPayload; |
| 130 | + if (!decoded.userId || typeof decoded.userId !== 'string') { |
| 131 | + throw new Error("Invalid token: missing or invalid userId"); |
| 132 | + } |
| 133 | + return { userId: decoded.userId }; |
| 134 | +}; |
| 135 | +``` |
| 136 | + |
| 137 | +**Important:** Always set `JWT_SECRET` as an environment variable and never commit it to version control. |
| 138 | + |
| 139 | +#### 4. Auth Middleware |
| 140 | + |
| 141 | +The auth middleware extracts the JWT token from the `Authorization` header and loads the user into `req.user`. |
| 142 | + |
| 143 | +```typescript |
| 144 | +// src/middleware/auth.ts |
| 145 | +export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { |
| 146 | + const authHeader = req.headers.authorization; |
| 147 | + |
| 148 | + if (!authHeader || !authHeader.startsWith('Bearer ')) { |
| 149 | + return next(); // Continue without user (for optional auth routes) |
| 150 | + } |
| 151 | + |
| 152 | + const token = authHeader.substring(7); |
| 153 | + |
| 154 | + try { |
| 155 | + const { userId } = verifyToken(token); |
| 156 | + const user = await userService.getUserById(userId); |
| 157 | + |
| 158 | + if (user) { |
| 159 | + req.user = user; |
| 160 | + } |
| 161 | + } catch (error) { |
| 162 | + // Invalid token - continue without user |
| 163 | + } |
| 164 | + |
| 165 | + next(); |
| 166 | +}; |
| 167 | +``` |
| 168 | + |
| 169 | +#### 5. Auth Guard |
| 170 | + |
| 171 | +The auth guard ensures that a user is authenticated before proceeding. |
| 172 | + |
| 173 | +```typescript |
| 174 | +export const authGuard = (req: Request, res: Response, next: NextFunction) => { |
| 175 | + if (!req.user) { |
| 176 | + return res.status(401).json({ error: "Unauthorized" }); |
| 177 | + } |
| 178 | + next(); |
| 179 | +}; |
| 180 | +``` |
| 181 | + |
| 182 | +### Route Configuration |
| 183 | + |
| 184 | +Routes are configured to use middleware appropriately: |
| 185 | + |
| 186 | +```typescript |
| 187 | +// Public routes (no auth required) |
| 188 | +app.get("/api/auth/offer", authController.getOffer); |
| 189 | +app.post("/api/auth", authController.login); |
| 190 | +app.post("/api/webhook", webhookController.handleWebhook); // Webhooks don't require auth |
| 191 | + |
| 192 | +// Protected routes (auth required) |
| 193 | +app.use(authMiddleware); // Apply auth middleware to all routes below |
| 194 | + |
| 195 | +app.get("/api/users/me", authGuard, userController.currentUser); |
| 196 | +app.post("/api/currencies", authGuard, currencyController.createCurrency); |
| 197 | +// ... other protected routes |
| 198 | +``` |
| 199 | + |
| 200 | +**Route patterns:** |
| 201 | +- **Public routes**: Authentication endpoints, webhooks, and any public-facing APIs |
| 202 | +- **Protected routes**: All routes after `app.use(authMiddleware)` require authentication |
| 203 | +- **Optional auth routes**: Routes that work with or without authentication (rare) |
| 204 | + |
| 205 | +### Environment Variables |
| 206 | + |
| 207 | +Required environment variables for authentication: |
| 208 | + |
| 209 | +```env |
| 210 | +# JWT secret for token signing/verification |
| 211 | +JWT_SECRET=your-secret-key-here |
| 212 | +
|
| 213 | +# Registry base URL for signature verification |
| 214 | +PUBLIC_REGISTRY_URL=https://registry.example.com |
| 215 | +``` |
| 216 | + |
| 217 | +### Next Steps |
| 218 | + |
| 219 | +1. **[Webhook Controller](./webhook-controller.md)** - How to handle incoming webhooks from the eVault system |
| 220 | +2. **[Mapping Rules](../../infrastructure/web3-adapter/MAPPING_RULES.md)** - How to create mappings between global ontology and your local database schema |
| 221 | + |
| 222 | +These components work together to create a seamless integration between your platform and the W3DS ecosystem. |
| 223 | + |
| 224 | + |
0 commit comments