diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index cb4f5b7d..a524446c 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -57,4 +57,10 @@ export interface Config { gitHub: GitHubConfig; registryConfig: RegistryConfig; auction: AuctionConfig; + turnkey: { + apiBaseUrl: string; + apiPublicKey: string; + apiPrivateKey: string; + defaultOrganizationId: string; + }; } diff --git a/packages/backend/src/routes/auth.ts b/packages/backend/src/routes/auth.ts index fbe9f137..3c4aa4bb 100644 --- a/packages/backend/src/routes/auth.ts +++ b/packages/backend/src/routes/auth.ts @@ -1,50 +1,50 @@ import { Router } from 'express'; import { SiweMessage } from 'siwe'; import { Service } from '../service'; -// import { authenticateUser, createUser } from '../turnkey-backend'; +import { authenticateUser, createUser } from '../turnkey-backend'; const router = Router(); // // Turnkey // -// router.get('/registration/:email', async (req, res) => { -// const service: Service = req.app.get('service'); -// const user = await service.getUserByEmail(req.params.email); -// if (user) { -// return res.send({ subOrganizationId: user?.subOrgId }); -// } else { -// return res.sendStatus(204); -// } -// }); +router.get('/registration/:email', async (req, res) => { + const service: Service = req.app.get('service'); + const user = await service.getUserByEmail(req.params.email); + if (user) { + return res.send({ subOrganizationId: user?.subOrgId }); + } else { + return res.sendStatus(204); + } +}); -// router.post('/register', async (req, res) => { -// console.log('Register', req.body); -// const { email, challenge, attestation } = req.body; -// const user = await createUser(req.app.get('service'), { -// challenge, -// attestation, -// userEmail: email, -// userName: email.split('@')[0], -// }); -// req.session.address = user.id; -// res.sendStatus(200); -// }); +router.post('/register', async (req, res) => { + console.log('Register', req.body); + const { email, challenge, attestation } = req.body; + const user = await createUser(req.app.get('service'), { + challenge, + attestation, + userEmail: email, + userName: email.split('@')[0], + }); + req.session.address = user.id; + res.sendStatus(200); +}); -// router.post('/authenticate', async (req, res) => { -// console.log('Authenticate', req.body); -// const { signedWhoamiRequest } = req.body; -// const user = await authenticateUser( -// req.app.get('service'), -// signedWhoamiRequest, -// ); -// if (user) { -// req.session.address = user.id; -// res.sendStatus(200); -// } else { -// res.sendStatus(401); -// } -// }); +router.post('/authenticate', async (req, res) => { + console.log('Authenticate', req.body); + const { signedWhoamiRequest } = req.body; + const user = await authenticateUser( + req.app.get('service'), + signedWhoamiRequest, + ); + if (user) { + req.session.address = user.id; + res.sendStatus(200); + } else { + res.sendStatus(401); + } +}); // // SIWE Auth diff --git a/packages/backend/src/turnkey-backend.ts b/packages/backend/src/turnkey-backend.ts new file mode 100644 index 00000000..541f76ff --- /dev/null +++ b/packages/backend/src/turnkey-backend.ts @@ -0,0 +1,130 @@ +import { Turnkey, TurnkeyApiTypes } from '@turnkey/sdk-server'; + +// Default path for the first Ethereum address in a new HD wallet. +// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form: +// m / purpose' / coin_type' / account' / change / address_index +// - Purpose is a constant set to 44' following the BIP43 recommendation. +// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md +// - Account, Change, and Address Index are set to 0 +import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server'; +import { getConfig } from './utils'; +import { Service } from './service'; + +type TAttestation = TurnkeyApiTypes['v1Attestation']; + +type CreateUserParams = { + userName: string; + userEmail: string; + challenge: string; + attestation: TAttestation; +}; + +export async function createUser( + service: Service, + { userName, userEmail, challenge, attestation }: CreateUserParams, +) { + try { + if (await service.getUserByEmail(userEmail)) { + throw new Error(`User already exists: ${userEmail}`); + } + + const config = await getConfig(); + const turnkey = new Turnkey(config.turnkey); + + const apiClient = turnkey.api(); + + const walletName = `Default ETH Wallet`; + + const createSubOrgResponse = await apiClient.createSubOrganization({ + subOrganizationName: `Default SubOrg for ${userEmail}`, + rootQuorumThreshold: 1, + rootUsers: [ + { + userName, + userEmail, + apiKeys: [], + authenticators: [ + { + authenticatorName: 'Passkey', + challenge, + attestation, + }, + ], + }, + ], + wallet: { + walletName: walletName, + accounts: DEFAULT_ETHEREUM_ACCOUNTS, + }, + }); + + const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId); + const wallet = refineNonNull(createSubOrgResponse.wallet); + + const result = { + id: wallet.walletId, + address: wallet.addresses[0], + subOrgId: subOrgId, + }; + console.log('Turnkey success', result); + + const user = await service.createUser({ + name: userName, + email: userEmail, + subOrgId, + ethAddress: wallet.addresses[0], + turnkeyWalletId: wallet.walletId, + }); + console.log('New user', user); + + return user; + } catch (e) { + console.error('Failed to create user:', e); + throw e; + } +} + +export async function authenticateUser( + service: Service, + signedWhoamiRequest: { + url: string; + body: any; + stamp: { + stampHeaderName: string; + stampHeaderValue: string; + }; + }, +) { + try { + const tkRes = await fetch(signedWhoamiRequest.url, { + method: 'POST', + body: signedWhoamiRequest.body, + headers: { + [signedWhoamiRequest.stamp.stampHeaderName]: + signedWhoamiRequest.stamp.stampHeaderValue, + }, + }); + console.log('AUTH RESULT', tkRes.status); + if (tkRes.status !== 200) { + console.log(await tkRes.text()); + return null; + } + const orgId = (await tkRes.json()).organizationId; + const user = await service.getUserBySubOrgId(orgId); + return user; + } catch (e) { + console.error('Failed to authenticate:', e); + throw e; + } +} + +function refineNonNull( + input: T | null | undefined, + errorMessage?: string, +): T { + if (input == null) { + throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`); + } + + return input; +}