Skip to content

Commit

Permalink
Merge pull request #4 from DIMO-Network/moiz/auth-endpoints
Browse files Browse the repository at this point in the history
JWT Flow Working with Hardcoded Creds
  • Loading branch information
MoizAhmedd authored Oct 28, 2024
2 parents 6100f29 + 4895106 commit 3ded940
Show file tree
Hide file tree
Showing 10 changed files with 10,871 additions and 5,305 deletions.
15,770 changes: 10,498 additions & 5,272 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@dimo-network/transactions": "^0.1.42",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@turnkey/http": "^2.15.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.114",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"buffer": "^6.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
Expand Down
21 changes: 21 additions & 0 deletions recoverAddress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/ethers@5.7.2/dist/ethers.umd.min.js";
script.onload = function() {
console.log("Ethers.js (UMD) loaded!");
const message = 'SIGNED MESSAGE';

// const hashedMessageWithoutPrefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(message));
const hashedMessageWithoutPrefix = ethers.utils.sha256(ethers.utils.toUtf8Bytes(message));


const signature = "SIGNATURE";

const recoveredAddress = ethers.utils.recoverAddress(hashedMessageWithoutPrefix, signature);
const expectedAddress = "EXPECTED ADDRESS";

console.log(recoveredAddress);
console.log(expectedAddress);

console.log(recoveredAddress == expectedAddress);
};
document.head.appendChild(script);
28 changes: 28 additions & 0 deletions recovery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Step 1: Define the signed message
const signedMessage = "Hello from DIMO";

// Step 2: Hash the message without Ethereum's prefix
const hashedMessageWithoutPrefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(signedMessage));

// Step 4: Extract r, s, and v values from the signature
let signatureR = "60bb900a556ead99e87f30bc9fb274d8426951a84d2258786f664a81266e8aec";
let signatureS = "4661155ee5182c87e8e584d244c4d51e4d089b9c516f8c34e8edfa42d306c508";
let signatureV = "01"; // v is in hexadecimal

// Step 5: Convert v from hex to decimal and adjust for Ethereum signature recovery
signatureV = parseInt(signatureV, 16); // Convert hex "01" to decimal 1

// Step 6: Ensure r and s are 0x-prefixed and in the correct format
signatureR = "0x" + signatureR;
signatureS = "0x" + signatureS;

// Step 7: Try recovering the address with v = 27
let signatureV27 = signatureV + 27; // v = 27
const combinedSignatureV27 = ethers.utils.joinSignature({ r: signatureR, s: signatureS, v: signatureV27 });

console.log(combinedSignatureV27);
const recoveredAddress = ethers.utils.recoverAddress(hashedMessageWithoutPrefix, combinedSignatureV27);
const expectedAddress = "0xB1E674372d4A9cA625a4f8dfA0E41493C3f8b9ca";

console.log(recoveredAddress);

1 change: 1 addition & 0 deletions src/components/Auth/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const EmailInput: React.FC<EmailInputProps> = ({

const result = await sendOtp(email);

console.log(result);
if (result.success && result.otpId) {
setOtpId(result.otpId); // Store the otpId
setAuthStep(1); // Move to OTP input step
Expand Down
26 changes: 22 additions & 4 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@

import React, { createContext, useContext, ReactNode, useState } from "react";
import {
createAccount,
fetchUserDetails,
sendOtp,
verifyOtp,
} from "../services/accountsService"; // Import the service functions
import { authenticateUser } from "../utils/authUtils";
import { UserObject } from "../models/user";
import { createPasskey } from "../services/turnkeyService";

interface AuthContextProps {
sendOtp: (
Expand Down Expand Up @@ -67,10 +69,26 @@ export const AuthProvider = ({
if (result.success && result.otpId) {
console.log(`OTP sent to ${email}, OTP ID: ${result.otpId}`);
return { success: true, otpId: result.otpId };
}
} else {
//Need to trigger account creation

console.log(`OTP sent to ${email}`);
return { success: false };
//Create passkey and get attestation
const resp = await createPasskey(email);

const attestation = resp[0];
const challenge = resp[1];

//Trigger account creation request
const account = await createAccount(email, attestation as object, challenge as string, true);

//Send OTP Again
const newOtp = await sendOtp(email); // Call the updated sendOtp service
if ( newOtp.success && newOtp.otpId) {
console.log("YES");
return { success: true, otpId: newOtp.otpId };
}
return { success: false };
}
} catch (err) {
setError("Failed to send OTP");
console.error(err);
Expand Down Expand Up @@ -124,7 +142,7 @@ export const AuthProvider = ({
const user = userDetailsResponse.user;
console.log(user);
setUser(user); // Store the user object in the context
authenticateUser(email, (token) => {
await authenticateUser(email, user.subOrganizationId, user.walletAddress, user!.smartContractAddress!, (token) => {
onSuccess(token);
});
} catch (error) {
Expand Down
38 changes: 36 additions & 2 deletions src/services/accountsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,40 @@ export const verifyOtp = async (
return { success: true, credentialBundle: data.credentialBundle };
};

export const verifyEmail = async (
email: string,
encodedChallenge: string,
attestation: object,
): Promise<{success: boolean, credentialBundle?: string; error?: string}> => {
// Call Turnkey's OTP verification API/SDK
//Endpoint: PUT /api/auth/otp
const response = await fetch(`${DIMO_ACCOUNTS_BASE_URL}/account/verify-email`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
encodedChallenge,
attestation,
}),
});

// Handle response failure cases first
if (!response.ok) {
throw new Error("Failed to send OTP");
}

// // Return success with OTP ID
return { success: true };
};

// Function to create an account
export const createAccount = async (
email: string
email: string,
attestation?: object,
challenge?: string,
deployAccount?: boolean,
): Promise<{ success: boolean }> => {
const response = await fetch(`${DIMO_ACCOUNTS_BASE_URL}/account`, {
method: "POST",
Expand All @@ -93,7 +124,10 @@ export const createAccount = async (
},
body: JSON.stringify({
email,
key: process.env.DIMO_API_KEY, //TODO: Fetch from dev props
key: "d794016835909c49dd94d65ea06c12b428761550f187ecc765732cd6e823286b", //TODO: Fetch from dev props
attestation,
encodedChallenge: challenge,
deployAccount: true,
}),
});

Expand Down
73 changes: 70 additions & 3 deletions src/services/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,74 @@
* This service should be imported and called from components or hooks that handle authentication logic.
*/

const DIMO_AUTH_BASE_URL = process.env.REACT_APP_DIMO_AUTH_URL || 'https://auth.dev.dimo.zone';
const DIMO_AUTH_BASE_URL = 'https://auth.dev.dimo.zone'; //TODO: Pull from env and be able to toggle

export const generateChallenge = async (clientId: string, domain: string, scope: string, address: string): Promise<{ success: boolean; error?: string; data?: any }> => {
try {
const queryParams = new URLSearchParams({
client_id: clientId,
domain: domain,
scope: scope,
response_type: "code",
address: address,
});

const response = await fetch(`${DIMO_AUTH_BASE_URL}/auth/web3/generate_challenge`, {
method: "POST", // Changed to GET since we are passing query params
body: queryParams,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});

if (!response.ok) {
const errorData = await response.json();
return { success: false, error: errorData.message || 'Failed to generate challenge' };
}

const data = await response.json();
return { success: true, data: data };
} catch (error) {
console.error('Error generating challenge:', error);
return { success: false, error: 'An error occurred while generating challenge' };
}
};


export const submitWeb3Challenge = async (
clientId: string,
state: string,
domain: string,
signature: string
): Promise<{ success: boolean; error?: string; data?: any }> => {
try {
// Construct the body using URLSearchParams for form-urlencoded
const formBody = new URLSearchParams({
client_id: clientId,
state: state,
grant_type: "authorization_code", // Fixed value
domain: domain,
signature: signature, // The 0x-prefixed signature obtained from Step 2
});

const response = await fetch(`${DIMO_AUTH_BASE_URL}/auth/web3/submit_challenge`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formBody, // Send the form-encoded body
});

if (!response.ok) {
const errorData = await response.json();
return { success: false, error: errorData.message || 'Failed to submit challenge' };
}

const data = await response.json();
return { success: true, data };
} catch (error) {
console.error('Error submitting web3 challenge:', error);
return { success: false, error: 'An error occurred while submitting challenge' };
}
};

// Add an empty export to make it a module
export {};
100 changes: 96 additions & 4 deletions src/services/turnkeyService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,102 @@
/**
* turnkeyService.ts
*
*
* This service handles all actions dependent on turnkey
* using the Turnkey Client Libraries, or custom Dimo SDK's such as the transactions SDK
*
* Specific Responsibilities include: Signing Messages, Triggering OTP's etc
*
* Specific Responsibilities include: Signing Messages, Triggering OTP's etc
*/

export {}
import {
KernelSigner,
newKernelConfig,
sacdPermissionValue,
} from "@dimo-network/transactions";
import {
getWebAuthnAttestation,
} from "@turnkey/http";
import { IframeStamper } from "@turnkey/iframe-stamper";
import { WebauthnStamper } from "@turnkey/webauthn-stamper";
import {
base64UrlEncode,
generateRandomBuffer,
} from "../utils/authUtils";
import { verifyEmail } from "./accountsService";

const stamper = new WebauthnStamper({
rpId: "ab1a735dff55.ngrok.app", //TODO: Should not be hardcoded
});

const kernelSignerConfig = newKernelConfig({
rpcUrl:
"https://polygon-amoy.g.alchemy.com/v2/-0PsUljNtSdA31-XWj-kL_L1Mx2ArYfS", //TODO: Store in ENV
bundlerUrl:
"https://rpc.zerodev.app/api/v2/bundler/f4d1596a-edfd-4063-8f99-2d8835e07739", //TODO: Store in ENV
paymasterUrl:
"https://rpc.zerodev.app/api/v2/paymaster/f4d1596a-edfd-4063-8f99-2d8835e07739", //TODO: Store in ENV
environment: "dev", // omit this to default to prod
});

let kernelSigner: KernelSigner;

export const createPasskey = async (email: string) => {
const challenge = generateRandomBuffer();
const authenticatorUserId = generateRandomBuffer();

// An example of possible options can be found here:
// https://www.w3.org/TR/webauthn-2/#sctn-sample-registration
const attestation = await getWebAuthnAttestation({
publicKey: {
rp: {
id: "ab1a735dff55.ngrok.app",
name: "Dimo Passkey Wallet",
},
challenge,
pubKeyCredParams: [
{
type: "public-key",
alg: -7,
},
],
user: {
id: authenticatorUserId,
name: email,
displayName: email,
},
authenticatorSelection: {
requireResidentKey: true,
residentKey: "required",
userVerification: "preferred",
},
},
});

return [attestation, base64UrlEncode(challenge)];
};

export const initializePasskey = async (
subOrganizationId: string,
walletAddress: string
) => {
kernelSigner = new KernelSigner(kernelSignerConfig);
await kernelSigner.passkeyInit(
subOrganizationId,
walletAddress as `0x${string}`,
stamper
);
};

export const signChallenge = async (
challenge: string,
organizationId: string,
walletAddress: string
) => {

//This is triggering a turnkey API request to sign a raw payload
//Notes on signature, turnkey api returns an ecdsa signature, which the kernel client is handling
const signature = await kernelSigner.kernelClient.signMessage({
message: challenge,
});

return signature;
};
Loading

0 comments on commit 3ded940

Please sign in to comment.