Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Running into an issue with passkeys and viem. #177

Open
Watcher-eth opened this issue Nov 28, 2023 · 6 comments
Open

Running into an issue with passkeys and viem. #177

Watcher-eth opened this issue Nov 28, 2023 · 6 comments

Comments

@Watcher-eth
Copy link

Ive been unsuccesfully trying to integrate your anagram pwa passkey example for a couple weeks now but im stuck. I set everything up to instructions however already had issues when trying to generate the public/private keypair through the cli. I managed to create a public a api key and a private key through the dashboard but cant get passkeys working. Ill attach my code butI have pretty muchs trcitly stuck to the anagram demo. Im either getting a parsing error - Turnkey error 3 or a Turnkey error 16: no valid user found for authenticator: rpc error: code = Unauthenticated desc = no valid authentication signature found for request:.

I have also tried using the new ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V3 but that didnt work either. Help would be very appreicated.

//create user api route
import type { NextApiRequest, NextApiResponse } from 'next'
import { getAuth } from '@clerk/nextjs/server'
import { TurnkeyApiTypes, TurnkeyClient } from '@turnkey/http'
import { createActivityPoller } from '@turnkey/http/dist/async'
import { ApiKeyStamper } from '@turnkey/api-key-stamper'
import { users } from '@shared/db/schema'
import { db } from '@shared/db/drizzle'
import { eq } from 'drizzle-orm'
import { first } from 'lodash'
import { refineNonNull } from '@shared/client-utils'
import * as dotenv from 'dotenv'

dotenv.config({ path: '../../../.env.local' }) // Replace with the actual path to your .env file

type TAttestation = TurnkeyApiTypes['v1Attestation']

type CreateSubOrgRequest = {
subOrgName: string
challenge: string
attestation: TAttestation
}

type CreateSubOrgResponse = {
subOrgId: string
addresses: Array<{ format?: any; address?: string }>
createdAddress: { format?: any; address?: string }
privateKeyId: string
}

type ErrorMessage = {
message: string
}

/**

  • For a new user:

  • Creates a sub org on TurnKey for the user and it's first key pair

  • After this function runs successfully, we'll have a new secure non-custodial wallet for the user ready to use.

  • @param req

  • @param res

  • @returns
    */
    export default async function createUser(
    req: NextApiRequest,
    res: NextApiResponse<CreateSubOrgResponse | ErrorMessage>,
    ) {
    try {
    const { userId } = getAuth(req)

    if (!userId) {
    return res.status(401).json({ message: 'Unauthorized' })
    }

    const createSubOrgRequest = req.body as CreateSubOrgRequest
    // Log out API environment variables for debugging
    console.log(
    'NEXT_PUBLIC_TURNKEY_API_BASE_URL:',
    process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL,
    req.body,
    )
    console.log(
    'NEXT_PUBLIC_TURNKEY_API_PUBLIC_KEY:',
    process.env.NEXT_PUBLIC_TURNKEY_API_PUBLIC_KEY,
    )
    console.log(
    'NEXT_PUBLIC_TURNKEY_API_PRIVATE_KEY:',
    process.env.NEXT_PUBLIC_TURNKEY_API_PRIVATE_KEY,
    )
    console.log(
    'NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID:',
    process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID,
    )
    console.log(
    'NEXT_PUBLIC_TURNKEY_BACKEND_API_KEY_NAME:',
    process.env.NEXT_PUBLIC_TURNKEY_BACKEND_API_KEY_NAME,
    )

    const turnkeyClient = new TurnkeyClient(
    { baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL! },
    new ApiKeyStamper({
    apiPublicKey: process.env.NEXT_PUBLIC_TURNKEY_API_PUBLIC_KEY!,
    apiPrivateKey: process.env.NEXT_PUBLIC_TURNKEY_API_PRIVATE_KEY!,
    }),
    )

    const activityPoller = createActivityPoller({
    client: turnkeyClient,
    requestFn: turnkeyClient.createSubOrganization,
    })

    // Create sub org on turnkey for user...
    const completedActivity = await activityPoller({
    type: 'ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V2',
    timestampMs: String(Date.now()),
    organizationId: process.env.NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID!,
    parameters: {
    subOrganizationName: createSubOrgRequest.subOrgName,

     rootQuorumThreshold: 1,
     rootUsers: [
       {
         userName: 'New user',
         apiKeys: [],
         authenticators: [
           {
             authenticatorName: 'Passkey',
             challenge: createSubOrgRequest.challenge,
             attestation: createSubOrgRequest.attestation,
           },
         ],
       },
       {
         userName: 'onboarding-helper',
         userEmail: '[email protected]',
         authenticators: [],
         apiKeys: [
           {
             apiKeyName:
               process.env.NEXT_PUBLIC_TURNKEY_BACKEND_API_KEY_NAME!,
             publicKey: process.env.NEXT_PUBLIC_TURNKEY_API_PUBLIC_KEY!,
           },
         ],
       },
     ],
    

    },
    })
    console.log(
    'Turnkey Request Payload:',
    JSON.stringify(completedActivity, null, 2),
    )

    const subOrgId = refineNonNull(
    completedActivity.result.createSubOrganizationResult?.subOrganizationId,
    )

    if (!subOrgId) {
    // Handle the error or log a message
    console.error('Invalid subOrgId:', subOrgId)
    return res.status(500).json({ message: 'Invalid subOrgId' })
    }

    const activityPollerForCreatePrivateKeys = createActivityPoller({
    client: turnkeyClient,
    requestFn: turnkeyClient.createPrivateKeys,
    })

    // Ask turnkey to create a new public/private key for the user...
    const createKeyPairForSubOrg = await activityPollerForCreatePrivateKeys({
    type: 'ACTIVITY_TYPE_CREATE_PRIVATE_KEYS_V2',
    organizationId: subOrgId,
    timestampMs: String(Date.now()),
    parameters: {
    privateKeys: [
    {
    privateKeyName: ETH Key ${Math.floor(Math.random() * 1000)},
    curve: 'CURVE_SECP256K1',
    addressFormats: ['ADDRESS_FORMAT_ETHEREUM'],
    privateKeyTags: [],
    },
    ],
    },
    })
    console.log(
    'Turnkey Response:',
    JSON.stringify(createKeyPairForSubOrg, null, 2),
    )

    const createSubOrgKeyPairResult = first(
    createKeyPairForSubOrg.result.createPrivateKeysResultV2?.privateKeys,
    )

    const addresses = refineNonNull(createSubOrgKeyPairResult?.addresses)
    // On initial suborg bootstrap, this is always the first key
    const createdAddress = refineNonNull(first(addresses))
    // Get the private key id (not the private key itself) to save to the database.
    // We use this private key id on the TurnKey API to request and sign transactions
    const privateKeyId = refineNonNull(createSubOrgKeyPairResult?.privateKeyId)

    await db
    .update(users)
    .set({
    turnkey_suborg: subOrgId,
    turnkey_private_key_id: privateKeyId,
    turnkey_private_key_public_address: createdAddress?.address,
    })
    .where(eq(users.external_auth_provider_user_id, userId))

    res.status(200).json({
    subOrgId,
    createdAddress,
    addresses: addresses,
    privateKeyId: privateKeyId,
    })
    } catch (e) {
    console.error('Error in createUser:', e)
    res.status(500).json({
    message: 'Something went wrong.',
    error: e.message, // Add more details if needed
    })
    }
    }

// ENV (Will update the actual codes for production this is just to make it easier to find my error)
NEXT_PUBLIC_TURNKEY_API_PUBLIC_KEY: 02811e22530d37b02a98409fa97e5b66d76c121655d28d0db39dd395060b67e83e
NEXT_PUBLIC_TURNKEY_API_PRIVATE_KEY: 159363b1f56c4c50a93c51630cc8ea8f
NEXT_PUBLIC_TURNKEY_ORGANIZATION_ID: ea4f6d5e-9911-4f35-b442-bd81451200e7
NEXT_PUBLIC_TURNKEY_BACKEND_API_KEY_NAME: api-demo

@andrewkmin
Copy link
Collaborator

Hi @Watcher-eth, thanks for reaching out.

First of all, sorry to hear this! already had issues when trying to generate the public/private keypair through the cli. Are there any specific errors you encountered? Would love to help iron those out.

Otherwise, a quick sanity check -- are you able to run any examples, like this one? Alternatively, are you able to successfully query our whoami endpoint? Just want to verify that the credentials are valid.

Also, what activity are you attempting when you run into Turnkey error 3 or a Turnkey error 16: no valid user found for authenticator: rpc error: code = Unauthenticated desc = no valid authentication signature found for request? We typically see that when an authenticator (e.g. api key) is not associated with the specified org ID.

@Watcher-eth
Copy link
Author

Hey so I haven't tried any other examples yet because I am building a pwa so I figured that would be the perfect example. Are there any good ones you can point me to? I basically have the following specifications:

Im building a consumer social app and we want to support onboarding both for existing wallets (we have implemented connect kit) and also new users through passkeys (side question is it possible to allow existing wallets to use passkeys for signing as well?).

For new users I want to be able to issue them a new wallet with passkeys and then store their keys to build a seamless UX.

I will also attach some of my current code so you can maybe understand my error better:

//api/turnkey/create-user
require('dotenv').config()

import type { NextApiRequest, NextApiResponse } from 'next'
import { TSignedRequest, TurnkeyClient } from '@turnkey/http'
import axios from 'axios'
import { ApiKeyStamper } from '@turnkey/api-key-stamper'
import type { TActivityResponse } from '@turnkey/http/dist/shared'
import * as dotenv from 'dotenv'

dotenv.config({ path: '../../../.env.local' }) // Replace with the actual path to your .env file

type TResponse = {
message: string
address?: string
privateKeyId?: string
}

export default async function createKey(
req: NextApiRequest,
res: NextApiResponse,
) {
let signedRequest = req.body as TSignedRequest

try {
const activityResponse = await axios.post(
signedRequest.url,
signedRequest.body,
{
headers: {
[signedRequest.stamp.stampHeaderName]:
signedRequest.stamp.stampHeaderValue,
},
},
)

if (activityResponse.status !== 200) {
  res.status(500).json({
    message: `expected 200, got ${activityResponse.status}`,
  })
}

let response = activityResponse.data as TActivityResponse
let attempts = 0
while (attempts < 3) {
  if (response.activity.status != 'ACTIVITY_STATUS_COMPLETED') {
    const stamper = new ApiKeyStamper({
      apiPublicKey: process.env.NEXT_PUBLIC_TURNKEY_API_PUBLIC_KEY!,
      apiPrivateKey: process.env.NEXT_PUBLIC_TURNKEY_API_PRIVATE_KEY!,
    })
    const client = new TurnkeyClient(
      { baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL! },
      stamper,
    )
    response = await client.getActivity({
      organizationId: response.activity.organizationId,
      activityId: response.activity.id,
    })
    attempts++
  } else {
    const privateKeys =
      response.activity.result.createPrivateKeysResultV2?.privateKeys

    // XXX: sorry for the ugly code! We expect a single key / address returned.
    // If we have more than one key / address returned, or none, this would break.
    const address = privateKeys
      ?.map((pk) => pk.addresses?.map((addr) => addr.address).join(''))
      .join('')
    const privateKeyId = privateKeys?.map((pk) => pk.privateKeyId).join('')

    res.status(200).json({
      message: 'successfully created key',
      address: address,
      privateKeyId: privateKeyId,
    })
    return
  }
}

} catch (e) {
console.error(e)

res.status(500).json({
  message: `Something went wrong, caught error: ${e}`,
})

}
}

//Client
const createSubOrgMutation = useMutation({
mutationFn: async () => {
const subOrgId = await attestUserAndCreateSubOrg({
passKeyIdName: 'Blitz',
subOrgName: 'Blitz',
})
await userDataQuery.refetch()
return subOrgId
},
})

const handleCreateSubOrgClick = async () => {
const subOrgId = await createSubOrgMutation.mutateAsync()
}

@andrewkmin
Copy link
Collaborator

andrewkmin commented Nov 29, 2023

@Watcher-eth can you try running this one locally https://github.com/tkhq/demo-viem-passkeys ? Just to sanity check that your authenticator/orgs are correctly configured.

Assuming that's the case, my hunch would be that there might be a mismatch when you attempt to authenticate a request using a passkey/credential for a specific user/org. I haven't had the chance to dig in too deeply here with this integrated example, but will try to investigate further in a bit

(side question is it possible to allow existing wallets to use passkeys for signing as well?)

By existing wallets, are you referring to users' self-custody wallets (such as Metamask/Coinbase Wallet/etc)? If so, unfortunately not.

For new users I want to be able to issue them a new wallet with passkeys and then store their keys to build a seamless UX.

This sounds great! 🙌

@Watcher-eth
Copy link
Author

Thank you very much my keys were wrong and I had to update my api but I got it working now. However that brings me to the next question. I have my wallet, my subOrgId and my private key all stored correctly in my DB now. However when I try to sign a message I get the following error.

Using this function:
export const signMessage = async (
data,
subOrgId: string,
privateKeyId: string,
privateKeyPublicAddress: string,
) => {
if (!subOrgId || !privateKeyId || !privateKeyPublicAddress) {
throw new Error('sub-org id or private key not found')
}
const viemAccount = await createAccount({
client: passkeyHttpClient,
organizationId: subOrgId,
privateKeyId: privateKeyId,
ethereumAddress: privateKeyPublicAddress,
})

const viemClient = createWalletClient({
account: viemAccount,
chain: polygon,
transport: http(),
})

const signedMessage = await viemClient.signMessage({
message: data,
})

return {
message: data,
signature: signedMessage,
}
}

Im getting this error every time:
TurnkeyActivityError: Failed to sign: Turnkey error 16: no valid user found for authenticator: rpc error: code = Unauthenticated desc = no valid authentication signature found for request: rpc error: code = InvalidArgument desc = cannot fetch authenticator for current user and credential ID

Also when I am prompted to sign with Passkeys I get to choose and I have a lot of them with the same name and im not sure if it matters which one I use? I tried multiple but non of them worked.

@Watcher-eth
Copy link
Author

Thank you again for your quick help! Maybe you can pinpoint me in the right direction again. Basically all I need once my users have created their wallet is a way to sign a Message and to signTypedData. (using the viem client) Ive set up two utility functions according to the viem documentation but they don't seem to be working.

// @ts-nocheck

import { TurnkeyClient } from '@turnkey/http'
import { createAccount } from '@turnkey/viem'
import { WebauthnStamper } from '@turnkey/webauthn-stamper'
import { createWalletClient, http } from 'viem'
import { polygon } from 'viem/chains'
const stamper = new WebauthnStamper({
rpId: global.location?.hostname,
})

const passkeyHttpClient = new TurnkeyClient(
{
baseUrl: process.env.NEXT_PUBLIC_TURNKEY_API_BASE_URL!,
},
stamper,
)

export const signMessage = async (
data,
subOrgId: string,
privateKeyId: string,
privateKeyPublicAddress: string,
) => {
if (!subOrgId || !privateKeyId || !privateKeyPublicAddress) {
throw new Error('sub-org id or private key not found')
}
const viemAccount = await createAccount({
client: passkeyHttpClient,
organizationId: subOrgId,
privateKeyId: privateKeyId,
ethereumAddress: privateKeyPublicAddress,
})

const viemClient = createWalletClient({
account: viemAccount,
chain: polygon,
transport: http(),
})

const signedMessage = await viemClient.signMessage({
message: data,
})

return {
message: data,
signature: signedMessage,
}
}

export const signTypedData = async (
domain,
types,
primaryType,
message,
subOrgId: string,
privateKeyId: string,
privateKeyPublicAddress: string,
) => {
if (!subOrgId || !privateKeyId || !privateKeyPublicAddress) {
throw new Error('sub-org id or private key not found')
}
const viemAccount = await createAccount({
client: passkeyHttpClient,
organizationId: subOrgId,
privateKeyId: privateKeyId,
ethereumAddress: privateKeyPublicAddress,
})

const viemClient = createWalletClient({
account: viemAccount,
chain: polygon,
transport: http(),
})

const signedMessage = await viemClient.signTypedData({
domain,
types,
primaryType,
message,
})

return {
message: message,
signature: signedMessage,
}
}

@andrewkmin
Copy link
Collaborator

Thank you again for your quick help! Maybe you can pinpoint me in the right direction again. Basically all I need once my users have created their wallet is a way to sign a Message and to signTypedData. (using the viem client) Ive set up two utility functions according to the viem documentation but they don't seem to be working.

Hey there, sorry for the delayed response. For the most part, that looks reasonable to me — can you share the error message you've been encountering? For reference, here's an example of how our Turnkey Viem client is created + invoked:

// 1. Sign a raw hex message

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants