added social auth example as reference implementation

This commit is contained in:
Samuel Andert 2023-07-21 09:02:42 +02:00
parent 68f85d9f29
commit 19b5dee9d8
16 changed files with 7119 additions and 0 deletions

View File

@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "prettier"]
}

32
example/social-auth/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel

View File

@ -0,0 +1,3 @@
node_modules/
dist/
.prettierrc

View File

@ -0,0 +1,6 @@
{
"arrowParens": "avoid",
"trailingComma": "es5",
"singleQuote": true,
"semi": true
}

View File

@ -0,0 +1,25 @@
# PKP x Google OAuth Web Example 🪢
This is an example web app that shows how you can mint and use programmable key pairs (PKPs) with just Google account. With PKPs, you can build distributed and customizable MPC wallets. Learn more about PKPs [here](https://developer.litprotocol.com/pkp/wallets/intro).
Check out the [live demo](https://pkp-social-auth-example.vercel.app/).
## 💻 Getting Started
1. Clone this repo and install dependencies:
```bash
git clone git@github.com:LIT-Protocol/pkp-social-auth-example.git
cd pkp-social-auth-example
yarn install
```
2. Start your development server:
```bash
yarn dev
```
3. Visit [http://localhost:3000](http://localhost:3000) to start playing with the app.

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}

5
example/social-auth/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

View File

@ -0,0 +1,28 @@
{
"name": "social-auth-next-example",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@lit-protocol/auth-helpers": "^2.2.45",
"@lit-protocol/lit-auth-client": "^2.2.45",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"eslint-config-prettier": "^8.8.0",
"ethers": "^5.7.2",
"next": "13.2.4",
"prettier": "^2.8.8",
"react": "18.2.0",
"react-dom": "18.2.0",
"wagmi": "^0.12.19"
},
"devDependencies": {
"@types/node": "18.15.11",
"@types/react": "18.0.31"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,66 @@
import { AppProps } from 'next/app';
import { WagmiConfig, createClient, configureChains } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect';
import { jsonRpcProvider } from 'wagmi/providers/jsonRpc';
import { Chain } from 'wagmi/chains';
const chronicleChain: Chain = {
id: 175177,
name: 'Chronicle',
network: 'chronicle',
nativeCurrency: {
decimals: 18,
name: 'Chronicle - Lit Protocol Testnet',
symbol: 'LIT',
},
rpcUrls: {
default: {
http: ['https://chain-rpc.litprotocol.com/http'],
},
public: {
http: ['https://chain-rpc.litprotocol.com/http'],
},
},
blockExplorers: {
default: {
name: 'Chronicle - Lit Protocol Testnet',
url: 'https://chain.litprotocol.com',
},
},
testnet: true,
};
const { provider, chains } = configureChains(
[chronicleChain],
[
jsonRpcProvider({
rpc: chain => ({ http: chain.rpcUrls.default.http[0] }),
}),
publicProvider(),
]
);
const client = createClient({
autoConnect: true,
connectors: [
new WalletConnectConnector({
chains,
options: {
projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID,
},
}),
],
provider,
});
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<WagmiConfig client={client}>
<Component {...pageProps} />
</WagmiConfig>
);
}

View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@ -0,0 +1,614 @@
import Head from 'next/head';
import { useCallback, useEffect, useState } from 'react';
import { LitNodeClient } from '@lit-protocol/lit-node-client';
import {
LitAuthClient,
BaseProvider,
GoogleProvider,
EthWalletProvider,
WebAuthnProvider,
isSignInRedirect,
getProviderFromUrl,
} from '@lit-protocol/lit-auth-client';
import { IRelayPKP, AuthMethod, SessionSigs } from '@lit-protocol/types';
import { ProviderType } from '@lit-protocol/constants';
import { ethers } from 'ethers';
import { useRouter } from 'next/router';
import { useConnect, useAccount, useDisconnect, Connector } from 'wagmi';
import {newSessionCapabilityObject, LitAccessControlConditionResource, LitAbility} from '@lit-protocol/auth-helpers';
// Local dev only: When using npm link, need to update encryption pkg to handle possible ipfs client init error
// let ipfsClient = null;
// try {
// ipfsClient = require("ipfs-http-client");
// } catch {}
enum Views {
SIGN_IN = 'sign_in',
HANDLE_REDIRECT = 'handle_redirect',
REQUEST_AUTHSIG = 'request_authsig',
REGISTERING = 'webauthn_registering',
REGISTERED = 'webauthn_registered',
AUTHENTICATING = 'webauthn_authenticating',
FETCHING = 'fetching',
FETCHED = 'fetched',
MINTING = 'minting',
MINTED = 'minted',
CREATING_SESSION = 'creating_session',
SESSION_CREATED = 'session_created',
ERROR = 'error',
}
export default function Dashboard() {
const redirectUri = 'http://localhost:3000';
const router = useRouter();
const [view, setView] = useState<Views>(Views.SIGN_IN);
const [error, setError] = useState<any>();
const [litAuthClient, setLitAuthClient] = useState<LitAuthClient>();
const [litNodeClient, setLitNodeClient] = useState<LitNodeClient>();
const [currentProviderType, setCurrentProviderType] =
useState<ProviderType>();
const [authMethod, setAuthMethod] = useState<AuthMethod>();
const [pkps, setPKPs] = useState<IRelayPKP[]>([]);
const [currentPKP, setCurrentPKP] = useState<IRelayPKP>();
const [sessionSigs, setSessionSigs] = useState<SessionSigs>();
const [message, setMessage] = useState<string>('Free the web!');
const [signature, setSignature] = useState<string>();
const [recoveredAddress, setRecoveredAddress] = useState<string>();
const [verified, setVerified] = useState<boolean>(false);
// Use wagmi to connect one's eth wallet
const { connectAsync, connectors } = useConnect({
onError(error) {
console.error(error);
setError(error);
},
});
const { isConnected, connector, address } = useAccount();
const { disconnectAsync } = useDisconnect();
/**
* Use wagmi to connect one's eth wallet and then request a signature from one's wallet
*/
async function handleConnectWallet(c: any) {
const { account, chain, connector } = await connectAsync(c);
try {
await authWithWallet(account, connector);
} catch (err) {
console.error(err);
setError(err);
setView(Views.ERROR);
}
}
/**
* Begin auth flow with Google
*/
async function authWithGoogle() {
setCurrentProviderType(ProviderType.Google);
const provider = litAuthClient.initProvider<GoogleProvider>(
ProviderType.Google
);
await provider.signIn();
}
/**
* Request a signature from one's wallet
*/
async function authWithWallet(address: string, connector: Connector) {
setView(Views.REQUEST_AUTHSIG);
// Create a function to handle signing messages
const signer = await connector.getSigner();
const signAuthSig = async (message: string) => {
const sig = await signer.signMessage(message);
return sig;
};
// Get auth sig
const provider = litAuthClient.getProvider(ProviderType.EthWallet);
const authMethod = await provider.authenticate({
address,
signMessage: signAuthSig,
});
setCurrentProviderType(ProviderType.EthWallet);
setAuthMethod(authMethod);
// Fetch PKPs associated with eth wallet account
setView(Views.FETCHING);
const pkps: IRelayPKP[] = await provider.fetchPKPsThroughRelayer(
authMethod
);
if (pkps.length > 0) {
setPKPs(pkps);
}
setView(Views.FETCHED);
}
async function registerWithWebAuthn() {
setView(Views.REGISTERING);
try {
// Register new PKP
const provider = litAuthClient.getProvider(
ProviderType.WebAuthn
) as WebAuthnProvider;
setCurrentProviderType(ProviderType.WebAuthn);
const options = await provider.register();
// Verify registration and mint PKP through relayer
const txHash = await provider.verifyAndMintPKPThroughRelayer(options);
setView(Views.MINTING);
const response = await provider.relay.pollRequestUntilTerminalState(
txHash
);
if (response.status !== 'Succeeded') {
throw new Error('Minting failed');
}
const newPKP: IRelayPKP = {
tokenId: response.pkpTokenId!,
publicKey: response.pkpPublicKey!,
ethAddress: response.pkpEthAddress!,
};
// Add new PKP to list of PKPs
const morePKPs: IRelayPKP[] = [...pkps, newPKP];
setCurrentPKP(newPKP);
setPKPs(morePKPs);
setView(Views.REGISTERED);
} catch (err) {
console.error(err);
setError(err);
setView(Views.ERROR);
}
}
async function authenticateWithWebAuthn() {
setView(Views.AUTHENTICATING);
try {
const provider = litAuthClient.getProvider(
ProviderType.WebAuthn
) as WebAuthnProvider;
const authMethod = await provider.authenticate();
setAuthMethod(authMethod);
// Authenticate with a WebAuthn credential and create session sigs with authentication data
setView(Views.CREATING_SESSION);
const litResource = new LitAccessControlConditionResource('*');
const sessionSigs = await provider.getSessionSigs({
pkpPublicKey: currentPKP.publicKey,
authMethod,
sessionSigsParams: {
chain: 'ethereum',
resourceAbilityRequests: [{
resource: litResource,
ability: LitAbility.PKPSigning
}],
},
});
setSessionSigs(sessionSigs);
setView(Views.SESSION_CREATED);
} catch (err) {
console.error(err);
setAuthMethod(null);
setError(err);
setView(Views.ERROR);
}
}
/**
* Handle redirect from Lit login server
*/
const handleRedirect = useCallback(
async (providerName: string) => {
setView(Views.HANDLE_REDIRECT);
try {
// Get relevant provider
let provider: BaseProvider;
if (providerName === ProviderType.Google) {
provider = litAuthClient.getProvider(ProviderType.Google);
}
setCurrentProviderType(providerName as ProviderType);
// Get auth method object that has the OAuth token from redirect callback
const authMethod: AuthMethod = await provider.authenticate();
setAuthMethod(authMethod);
// Fetch PKPs associated with social account
setView(Views.FETCHING);
const pkps: IRelayPKP[] = await provider.fetchPKPsThroughRelayer(
authMethod
);
if (pkps.length > 0) {
setPKPs(pkps);
}
setView(Views.FETCHED);
} catch (err) {
console.error(err);
setError(err);
setView(Views.ERROR);
}
// Clear url params once we have the OAuth token
// Be sure to use the redirect uri route
router.replace(window.location.pathname, undefined, { shallow: true });
},
[litAuthClient, router]
);
/**
* Mint a new PKP for current auth method
*/
async function mint() {
setView(Views.MINTING);
try {
// Mint new PKP
const provider = litAuthClient.getProvider(currentProviderType);
const txHash: string = await provider.mintPKPThroughRelayer(authMethod);
const response = await provider.relay.pollRequestUntilTerminalState(
txHash
);
if (response.status !== 'Succeeded') {
throw new Error('Minting failed');
}
const newPKP: IRelayPKP = {
tokenId: response.pkpTokenId,
publicKey: response.pkpPublicKey,
ethAddress: response.pkpEthAddress,
};
// Add new PKP to list of PKPs
const morePKPs: IRelayPKP[] = [...pkps, newPKP];
setPKPs(morePKPs);
setView(Views.MINTED);
setView(Views.CREATING_SESSION);
// Get session sigs for new PKP
await createSession(newPKP);
} catch (err) {
console.error(err);
setError(err);
setView(Views.ERROR);
}
}
/**
* Generate session sigs for current PKP and auth method
*/
async function createSession(pkp: IRelayPKP) {
setView(Views.CREATING_SESSION);
try {
const litResource = new LitAccessControlConditionResource('*');
// Get session signatures
const provider = litAuthClient.getProvider(currentProviderType);
const sessionSigs = await provider.getSessionSigs({
pkpPublicKey: pkp.publicKey,
authMethod,
sessionSigsParams: {
chain: 'ethereum',
resourceAbilityRequests: [{
resource: litResource,
ability: LitAbility.PKPSigning
}],
},
});
setCurrentPKP(pkp);
setSessionSigs(sessionSigs);
setView(Views.SESSION_CREATED);
} catch (err) {
console.error(err);
setError(err);
setView(Views.ERROR);
}
}
/**
* Sign a message with current PKP
*/
async function signMessageWithPKP() {
try {
const toSign = ethers.utils.arrayify(ethers.utils.hashMessage(message));
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();
`;
// Sign message
// @ts-ignore - complains about no authSig, but we don't need one for this action
const results = await litNodeClient.executeJs({
code: litActionCode,
sessionSigs: sessionSigs,
jsParams: {
toSign: toSign,
publicKey: currentPKP.publicKey,
sigName: 'sig1',
},
});
// Get signature
const result = results.signatures['sig1'];
const signature = ethers.utils.joinSignature({
r: '0x' + result.r,
s: '0x' + result.s,
v: result.recid,
});
setSignature(signature);
// Get the address associated with the signature created by signing the message
const recoveredAddr = ethers.utils.verifyMessage(message, signature);
setRecoveredAddress(recoveredAddr);
// Check if the address associated with the signature is the same as the current PKP
const verified =
currentPKP.ethAddress.toLowerCase() === recoveredAddr.toLowerCase();
setVerified(verified);
} catch (err) {
console.error(err);
setError(err);
setView(Views.ERROR);
}
}
useEffect(() => {
/**
* Initialize LitNodeClient and LitAuthClient
*/
async function initClients() {
try {
// Set up LitNodeClient and connect to Lit nodes
const litNodeClient = new LitNodeClient({
litNetwork: 'serrano',
debug: false,
});
await litNodeClient.connect();
setLitNodeClient(litNodeClient);
// Set up LitAuthClient
const litAuthClient = new LitAuthClient({
litRelayConfig: {
relayApiKey: 'test-api-key',
},
litNodeClient,
});
// Initialize providers
litAuthClient.initProvider<GoogleProvider>(ProviderType.Google);
litAuthClient.initProvider<EthWalletProvider>(ProviderType.EthWallet);
litAuthClient.initProvider<WebAuthnProvider>(ProviderType.WebAuthn);
setLitAuthClient(litAuthClient);
} catch (err) {
console.error(err);
setError(err);
setView(Views.ERROR);
}
}
if (!litNodeClient) {
initClients();
}
}, [litNodeClient]);
useEffect(() => {
// Check if app has been redirected from Lit login server
if (litAuthClient && !authMethod && isSignInRedirect(redirectUri)) {
const providerName = getProviderFromUrl();
handleRedirect(providerName);
}
}, [litAuthClient, handleRedirect, authMethod]);
if (!litNodeClient) {
return null;
}
return (
<>
<Head>
<title>Lit Auth Client</title>
<meta
name="description"
content="Create a PKP with just a Google account"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
{view === Views.ERROR && (
<>
<h1>Error</h1>
<p>{error.message}</p>
<button
onClick={() => {
if (sessionSigs) {
setView(Views.SESSION_CREATED);
} else {
if (authMethod) {
setView(Views.FETCHED);
} else {
setView(Views.SIGN_IN);
}
}
setError(null);
}}
>
Got it
</button>
</>
)}
{view === Views.SIGN_IN && (
<>
<h1>Sign in with Lit</h1>
{/* Since eth wallet is connected, prompt user to sign a message or disconnect their wallet */}
<>
{isConnected ? (
<>
<button
disabled={!connector.ready}
key={connector.id}
onClick={async () => {
setError(null);
await authWithWallet(address, connector);
}}
>
Continue with {connector.name}
</button>
<button
onClick={async () => {
setError(null);
await disconnectAsync();
}}
>
Disconnect wallet
</button>
</>
) : (
<>
{/* If eth wallet is not connected, show all login options */}
<button onClick={authWithGoogle}>Google</button>
{connectors.map(connector => (
<button
disabled={!connector.ready}
key={connector.id}
onClick={async () => {
setError(null);
await handleConnectWallet({ connector });
}}
>
{connector.name}
</button>
))}
<button onClick={registerWithWebAuthn}>
Register with WebAuthn
</button>
</>
)}
</>
</>
)}
{view === Views.HANDLE_REDIRECT && (
<>
<h1>Verifying your identity...</h1>
</>
)}
{view === Views.REQUEST_AUTHSIG && (
<>
<h1>Check your wallet</h1>
</>
)}
{view === Views.REGISTERING && (
<>
<h1>Register your passkey</h1>
<p>Follow your browser&apos;s prompts to create a passkey.</p>
</>
)}
{view === Views.REGISTERED && (
<>
<h1>Minted!</h1>
<p>
Authenticate with your newly registered passkey. Continue when
you&apos;re ready.
</p>
<button onClick={authenticateWithWebAuthn}>Continue</button>
</>
)}
{view === Views.AUTHENTICATING && (
<>
<h1>Authenticate with your passkey</h1>
<p>Follow your browser&apos;s prompts to create a passkey.</p>
</>
)}
{view === Views.FETCHING && (
<>
<h1>Fetching your PKPs...</h1>
</>
)}
{view === Views.FETCHED && (
<>
{pkps.length > 0 ? (
<>
<h1>Select a PKP to continue</h1>
{/* Select a PKP to create session sigs for */}
<div>
{pkps.map(pkp => (
<button
key={pkp.ethAddress}
onClick={async () => await createSession(pkp)}
>
{pkp.ethAddress}
</button>
))}
</div>
<hr></hr>
{/* Or mint another PKP */}
<p>or mint another one:</p>
<button onClick={mint}>Mint another PKP</button>
</>
) : (
<>
<h1>Mint a PKP to continue</h1>
<button onClick={mint}>Mint a PKP</button>
</>
)}
</>
)}
{view === Views.MINTING && (
<>
<h1>Minting your PKP...</h1>
</>
)}
{view === Views.MINTED && (
<>
<h1>Minted!</h1>
</>
)}
{view === Views.CREATING_SESSION && (
<>
<h1>Saving your session...</h1>
</>
)}
{view === Views.SESSION_CREATED && (
<>
<h1>Ready for the open web</h1>
<div>
<p>Check out your PKP:</p>
<p>{currentPKP.ethAddress}</p>
</div>
<hr></hr>
<div>
<p>Sign this message with your PKP:</p>
<p>{message}</p>
<button onClick={signMessageWithPKP}>Sign message</button>
{signature && (
<>
<h3>Your signature:</h3>
<p>{signature}</p>
<h3>Recovered address:</h3>
<p>{recoveredAddress}</p>
<h3>Verified:</h3>
<p>{verified ? 'true' : 'false'}</p>
</>
)}
</div>
</>
)}
</main>
</>
);
}

View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"target": "es5"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}