Update WebAuthn registration and authentication to use new and safer impl (#5)

* Update WebAuthn registration and authentication to use new and safer flow

* Implement usernameless WebAuthn registration + Authentication

The username is solely stored in the client side for the end-user to conveniently refer to.

* Use updated SDK interface

* Implement executeJs in webauthn demo

* Show executeJs sig

* Fix username bug

* encodeURIComponent for username

* Use latest working SDK version

* Use serrano
This commit is contained in:
Howard 2023-04-06 18:10:26 -07:00 committed by GitHub
parent efb3eab092
commit 2e19c39625
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 980 additions and 736 deletions

View File

@ -5,11 +5,12 @@
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@lit-protocol/access-control-conditions": "2.1.17",
"@lit-protocol/bls-sdk": "2.1.17",
"@lit-protocol/constants": "2.1.17",
"@lit-protocol/crypto": "2.1.17",
"@lit-protocol/lit-node-client": "2.1.17",
"@lit-protocol/access-control-conditions": "2.1.111",
"@lit-protocol/bls-sdk": "2.1.111",
"@lit-protocol/constants": "2.1.111",
"@lit-protocol/crypto": "2.1.111",
"@lit-protocol/lit-node-client": "2.1.111",
"@lit-protocol/lit-node-client-nodejs": "2.1.111",
"@mui/material": "^5.10.16",
"@react-oauth/google": "^0.4.0",
"@simplewebauthn/browser": "^6.2.2",

View File

@ -1,20 +1,18 @@
import * as LitJsSdk_accessControlConditions from "@lit-protocol/access-control-conditions";
import * as LitJsSdk_blsSdk from "@lit-protocol/bls-sdk";
import { AccsDefaultParams } from "@lit-protocol/constants";
import * as LitJsSdk from "@lit-protocol/lit-node-client";
import { Button, ButtonGroup } from "@mui/material";
import { AccsDefaultParams, JsonAuthSig } from "@lit-protocol/types";
import { Button, ButtonGroup, TextField } from "@mui/material";
import { GoogleLogin } from "@react-oauth/google";
import {
startAuthentication,
startRegistration,
} from "@simplewebauthn/browser";
import base64url from "base64url";
import { utils } from "ethers";
import { hexlify } from "ethers/lib/utils";
import { ethers, utils } from "ethers";
import { useState } from "react";
import "./App.css";
import { decodeAttestationObject } from "./utils/decodeAttestationObject";
import { parseAuthenticatorData } from "./utils/parseAuthenticatorData";
import { getDomainFromOrigin } from "./utils/string";
type CredentialResponse = any;
@ -28,21 +26,25 @@ const RELAY_API_URL =
process.env.REACT_APP_RELAY_API_URL || "http://localhost:3001";
function App() {
const [pkpEthAddress, setPkpEthAddress] = useState<string>("");
const [registeredPkpEthAddress, setRegisteredPkpEthAddress] = useState<
string
>("");
const [
googleCredentialResponse,
setGoogleCredentialResponse,
] = useState<CredentialResponse | null>(null);
const [pkpPublicKey, setPkpPublicKey] = useState<string>("");
const [registeredPkpPublicKey, setRegisteredPkpPublicKey] = useState<
string
>("");
const [authenticatedPkpPublicKey, setAuthenticatedPkpPublicKey] = useState<
string
>("");
const [status, setStatus] = useState("");
const [selectedAuthMethod, setSelectedAuthMethod] = useState(6);
const [
webAuthnCredentialPublicKey,
setWebAuthnCredentialPublicKey,
] = useState<string>("");
const [webAuthnSignature, setWebAuthnSignature] = useState<string>("");
const [webAuthnSignatureBase, setWebAuthnSignatureBase] = useState<string>(
""
const [webAuthnUsername, setWebAuthnUsername] = useState<string>("");
const [authSig, setAuthSig] = useState<JsonAuthSig | null>(null);
const [executeJsSignature, setExecuteJsSignature] = useState<string | null>(
null
);
const handleLoggedInToGoogle = async (
@ -61,8 +63,8 @@ function App() {
requestId,
setStatus,
({ pkpEthAddress, pkpPublicKey }) => {
setPkpEthAddress(pkpEthAddress);
setPkpPublicKey(pkpPublicKey);
setRegisteredPkpEthAddress(pkpEthAddress);
setRegisteredPkpPublicKey(pkpPublicKey);
}
);
};
@ -97,7 +99,7 @@ function App() {
{selectedAuthMethod === 6 && (
<>
<h3>
Step 1: log in with Google. Upon OAuth success, we will
Step 1: Log in with Google. Upon OAuth success, we will
mint a PKP on your behalf.
</h3>
<GoogleLogin
@ -107,8 +109,11 @@ function App() {
}}
useOneTap
/>
{pkpEthAddress && (
<div>PKP Eth Address: {pkpEthAddress}</div>
{registeredPkpEthAddress && (
<div>
Registered PKP Eth Address:{" "}
{registeredPkpEthAddress}
</div>
)}
<h3>
<s>
@ -128,8 +133,8 @@ function App() {
signatureBase: "dummy",
credentialPublicKey: "dummy",
},
pkpEthAddress,
pkpPublicKey
registeredPkpEthAddress,
registeredPkpPublicKey
)
}
>
@ -139,93 +144,88 @@ function App() {
)}
{selectedAuthMethod === 3 && (
<>
<h3>Step 1: Register using WebAuthn.</h3>
<h3>Step 1: Register to mint PKP. (optional username)</h3>
<TextField
label="Username"
variant="outlined"
onChange={e => setWebAuthnUsername(e.target.value)}
/>
<Button
variant="contained"
onClick={async () => {
await handleWebAuthnRegister(
webAuthnUsername,
setStatus,
({ attResp }) => {
const attestationObject = base64url.toBuffer(
attResp.response.attestationObject
);
const {
authData,
} = decodeAttestationObject(
window.cbor,
attestationObject
);
const parsedAuthData = parseAuthenticatorData(
window.cbor,
authData
);
console.log(
"storing credential public key in browser",
{
credentialPublicKey:
parsedAuthData.credentialPublicKey,
}
);
// set in local state
setWebAuthnCredentialPublicKey(
hexlify(
parsedAuthData.credentialPublicKey!
)
);
({ pkpEthAddress, pkpPublicKey }) => {
setRegisteredPkpEthAddress(pkpEthAddress);
setRegisteredPkpPublicKey(pkpPublicKey);
}
);
}}
>
Register
</Button>
<h3>Step 2: Authenticate using WebAuthn to mint PKP.</h3>
{registeredPkpEthAddress && (
<div>
<b>Registered PKP Eth Address: </b>
{registeredPkpEthAddress}
</div>
)}
<h3>
Step 2: Authenticate against Lit Nodes to generate auth
sigs.
</h3>
<Button
variant="contained"
onClick={async () => {
await handleWebAuthnAuthenticate(
setStatus,
webAuthnCredentialPublicKey,
({ pkpEthAddress, pkpPublicKey }) => {
setPkpEthAddress(pkpEthAddress);
setPkpPublicKey(pkpPublicKey);
},
setWebAuthnSignature,
setWebAuthnSignatureBase
);
const {
authSig,
pkpPublicKey,
} = await handleWebAuthnAuthenticate(setStatus);
setAuthSig(authSig);
// After authenticating, we can store the pkpPublicKey for executing a
// Lit Action later.
setAuthenticatedPkpPublicKey(pkpPublicKey);
}}
>
Authenticate
</Button>
{authenticatedPkpPublicKey && authSig && (
<>
<div>
<b>Authenticated PKP Public Key: </b>
{authenticatedPkpPublicKey}
</div>
<div>
<b>Auth Sig: </b>
{JSON.stringify(authSig)}
</div>
</>
)}
<h3>
<s>
Step 3: Use Lit Network to obtain a session sig and
then store an encryption condition.
</s>
(Session Sigs do not work currently.)
Step 3: Generate session signatures and use them to
execute a Lit Action.
</h3>
<Button
variant="contained"
onClick={() =>
handleStoreEncryptionCondition(
onClick={async () => {
const signature = await handleExecuteJs(
setStatus,
selectedAuthMethod,
googleCredentialResponse,
{
signature: webAuthnSignature,
signatureBase: webAuthnSignatureBase,
credentialPublicKey: webAuthnCredentialPublicKey,
},
pkpEthAddress,
pkpPublicKey
)
}
authSig!,
authenticatedPkpPublicKey
);
setExecuteJsSignature(signature);
}}
>
Encrypt With Lit
Execute Lit Action
</Button>
{executeJsSignature && (
<div>
<b>Executed Lit Action Signature: </b>
{executeJsSignature}
</div>
)}
</>
)}
</div>
@ -234,6 +234,44 @@ function App() {
export default App;
async function handleExecuteJs(
setStatusFn: (status: string) => void,
authSig: JsonAuthSig,
pkpPublicKey: string
): Promise<string> {
setStatusFn("Executing JS...");
const litActionCode = `
const go = async () => {
// this requests a signature share from the Lit Node
// the signature share will be automatically returned in the response from the node
// and combined into a full signature by the LitJsSdk for you to use on the client
// all the params (toSign, publicKey, sigName) are passed in from the LitJsSdk.executeJs() function
const sigShare = await LitActions.signEcdsa({ toSign, publicKey, sigName });
};
go();
`;
const litNodeClient = new LitJsSdk.LitNodeClient({
litNetwork: "serrano",
});
await litNodeClient.connect();
const results = await litNodeClient.executeJs({
code: litActionCode,
authSig,
// all jsParams can be used anywhere in your litActionCode
jsParams: {
// this is the string "Hello World" for testing
toSign: [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100],
publicKey: `0x${pkpPublicKey}`,
sigName: "sig1",
},
});
console.log("results: ", results);
return results.signatures["sig1"].signature;
}
async function mintPkpUsingRelayerGoogleAuthVerificationEndpoint(
credentialResponse: any,
setStatusFn: (status: string) => void
@ -244,6 +282,7 @@ async function mintPkpUsingRelayerGoogleAuthVerificationEndpoint(
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": "1234567890",
},
body: JSON.stringify({
idToken: credentialResponse.credential,
@ -262,38 +301,6 @@ async function mintPkpUsingRelayerGoogleAuthVerificationEndpoint(
}
}
async function mintPkpUsingRelayerWebAuthnVerificationEndpoint(
signature: string,
signatureBase: string,
credentialPublicKey: string,
setStatusFn: (status: string) => void
) {
setStatusFn("Minting PKP with relayer...");
const mintRes = await fetch(`${RELAY_API_URL}/auth/webauthn`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
signature,
signatureBase,
credentialPublicKey,
}),
});
if (mintRes.status < 200 || mintRes.status >= 400) {
console.warn("Something wrong with the API call", await mintRes.json());
setStatusFn("Uh oh, something's not quite right.");
return null;
} else {
const resBody = await mintRes.json();
console.log("Response OK", { body: resBody });
setStatusFn("Successfully initiated minting PKP with relayer.");
return resBody.requestId;
}
}
async function pollRequestUntilTerminalState(
requestId: string,
setStatusFn: (status: string) => void,
@ -313,7 +320,12 @@ async function pollRequestUntilTerminalState(
for (let i = 0; i < maxPollCount; i++) {
setStatusFn(`Waiting for auth completion (poll #${i + 1})`);
const getAuthStatusRes = await fetch(
`${RELAY_API_URL}/auth/status/${requestId}`
`${RELAY_API_URL}/auth/status/${requestId}`,
{
headers: {
"api-key": "1234567890",
},
}
);
if (getAuthStatusRes.status < 200 || getAuthStatusRes.status >= 400) {
@ -482,6 +494,7 @@ async function handleStoreEncryptionCondition(
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": "1234567890",
},
body: JSON.stringify({
key: hashedEncryptedSymmetricKeyStr,
@ -611,10 +624,24 @@ async function hashBytes({ bytes }: { bytes: Uint8Array }): Promise<string> {
// }
async function handleWebAuthnRegister(
username: string,
setStatusFn: (status: string) => void,
onSuccess: ({ attResp }: { attResp: any }) => void
onSuccess: ({
pkpEthAddress,
pkpPublicKey,
}: {
pkpEthAddress: string;
pkpPublicKey: string;
}) => void
) {
const resp = await fetch(`${RELAY_API_URL}/generate-registration-options`);
let url = `${RELAY_API_URL}/auth/webauthn/generate-registration-options`;
// Handle optional username
if (username !== "") {
url += `?username=${encodeURIComponent(username)}`;
}
const resp = await fetch(url, { headers: { "api-key": "1234567890" } });
let attResp;
try {
@ -635,110 +662,110 @@ async function handleWebAuthnRegister(
console.log("attResp", { attResp });
const verificationResp = await fetch(
`${RELAY_API_URL}/verify-registration`,
// Verify and mint PKP.
setStatusFn("Verifying WebAuthn registration...");
const verificationAndMintResp = await fetch(
`${RELAY_API_URL}/auth/webauthn/verify-registration`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": "1234567890",
},
body: JSON.stringify(attResp),
body: JSON.stringify({ credential: attResp }),
}
);
const verificationJSON = await verificationResp.json();
if (verificationJSON && verificationJSON.verified) {
setStatusFn("Successfully registered using WebAuthn!");
onSuccess({ attResp });
} else {
setStatusFn(
"Oh no, something went wrong during WebAuthn registration."
);
console.error("Error during WebAuthn registration", {
err: JSON.stringify(verificationJSON),
});
}
}
async function handleWebAuthnAuthenticate(
setStatusFn: (status: string) => void,
webAuthnCredentialPublicKey: string,
onSuccess: (resp: any) => void,
setWebAuthnSignatureFn: (signature: string) => void,
setWebAuthnSignatureBaseFn: (signatureBase: string) => void
if (
verificationAndMintResp.status < 200 ||
verificationAndMintResp.status >= 400
) {
const resp = await fetch(
`${RELAY_API_URL}/generate-authentication-options`
console.warn(
"Something went wrong with the API call",
await verificationAndMintResp.json()
);
let asseResp;
try {
const opts = await resp.json();
asseResp = await startAuthentication(opts);
} catch (error) {
// TODO: handle error
throw error;
setStatusFn("Uh oh, something's not quite right.");
return null;
}
const verificationResp = await fetch(
`${RELAY_API_URL}/verify-authentication`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(asseResp),
}
);
const verificationJSON = await verificationResp.json();
if (verificationJSON && verificationJSON.verified) {
setStatusFn("Successfully authenticated using WebAuthn!");
} else {
const resBody = await verificationAndMintResp.json();
console.log("Response OK", { body: resBody });
setStatusFn(
"Oh no, something went wrong during WebAuthn authentication."
);
console.error("Error during WebAuthn authentication", {
err: JSON.stringify(verificationJSON),
});
}
const clientDataHash = await crypto.subtle.digest(
"SHA-256",
base64url.toBuffer(asseResp.response.clientDataJSON)
);
const authDataBuffer = base64url.toBuffer(
asseResp.response.authenticatorData
);
const signatureBase = Buffer.concat([
authDataBuffer,
Buffer.from(clientDataHash),
]);
const signature = base64url.toBuffer(asseResp.response.signature);
// mint PKP using Relayer
console.log("Minting PKP using Relayer...", {
signature: hexlify(signature),
signatureBase: hexlify(signatureBase),
webAuthnCredentialPublicKey,
});
const requestId = await mintPkpUsingRelayerWebAuthnVerificationEndpoint(
hexlify(signature),
hexlify(signatureBase),
webAuthnCredentialPublicKey,
setStatusFn
"Successfully registered using WebAuthn! PKP minting initiated..."
);
// Poll until success
await pollRequestUntilTerminalState(requestId, setStatusFn, onSuccess);
// Update state
setWebAuthnSignatureFn(hexlify(signature));
setWebAuthnSignatureBaseFn(hexlify(signatureBase));
const mintRequestId = resBody.requestId;
await pollRequestUntilTerminalState(mintRequestId, setStatusFn, onSuccess);
}
const rpcUrl = process.env.REACT_APP_RPC_URL || "http://localhost:8545";
async function handleWebAuthnAuthenticate(
setStatusFn: (status: string) => void
): Promise<{
authSig: JsonAuthSig;
pkpPublicKey: string;
}> {
// Fetch latest blockHash
setStatusFn("Fetching latest block hash...");
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const block = await provider.getBlock("latest");
const blockHash = block.hash;
// Turn into byte array.
const blockHashBytes = ethers.utils.arrayify(blockHash);
console.log(
"blockHash",
blockHash,
blockHashBytes,
base64url(Buffer.from(blockHashBytes))
);
// Construct authentication options.
const rpId = getDomainFromOrigin(window.location.origin);
console.log("Using rpId: ", { rpId });
const authenticationOptions = {
challenge: base64url(Buffer.from(blockHashBytes)),
timeout: 60000,
userVerification: "required",
rpId,
};
// Authenticate with WebAuthn.
setStatusFn("Authenticating with WebAuthn...");
const authenticationResponse = await startAuthentication(
authenticationOptions
);
// BUG: We need to make sure userHandle is base64url encoded.
// Deep copy the authentication response.
const actualAuthenticationResponse = JSON.parse(
JSON.stringify(authenticationResponse)
);
actualAuthenticationResponse.response.userHandle = base64url.encode(
authenticationResponse.response.userHandle
);
// Call all nodes POST /web/auth/webauthn to generate authSig.
setStatusFn("Verifying WebAuthn authentication against Lit Network...");
const litNodeClient = new LitJsSdk.LitNodeClient({
litNetwork: "serrano",
});
await litNodeClient.connect();
// Generate authMethod.
const authMethod = litNodeClient.generateAuthMethodForWebAuthn(
actualAuthenticationResponse
);
// Get authSig.
const { authSig, pkpPublicKey } = await litNodeClient.signSessionKey({
authMethods: [authMethod],
expiration: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
resources: [],
});
return { authSig, pkpPublicKey };
}

6
src/utils/string.ts Normal file
View File

@ -0,0 +1,6 @@
export function getDomainFromOrigin(origin: string): string {
// remove protocol with regex
let newOrigin = origin.replace(/(^\w+:|^)\/\//, "");
// remove port with regex
return newOrigin.replace(/:\d+$/, "");
}

1250
yarn.lock

File diff suppressed because it is too large Load Diff