Skip to content

Commit

Permalink
feat: universal webauthn
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Nov 14, 2024
1 parent 59812a7 commit cf4fa68
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 19 deletions.
76 changes: 57 additions & 19 deletions src/Oddworld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,22 @@ export function create<
...Chains.Chain[],
] = typeof create.defaultParameters.chains,
>(parameters?: create.Parameters<chains>): Oddworld<chains>
export function create(
parameters: create.Parameters = create.defaultParameters,
): Oddworld {
const { chains, headless = true, transports, webauthn } = parameters
export function create(parameters?: create.Parameters | undefined): Oddworld {
const {
chains = create.defaultParameters.chains,
headless = create.defaultParameters.headless,
transports = create.defaultParameters.transports,
} = parameters ?? {}

const keystoreHost = (() => {
if (parameters?.keystoreHost) return parameters.keystoreHost
if (
typeof window !== 'undefined' &&
window.location.hostname === 'localhost'
)
return undefined
return create.defaultParameters.keystoreHost
})()

const store = createStore(
subscribeWithSelector(
Expand All @@ -57,7 +69,7 @@ export function create(
const { chain } = get()
return createClient({
chain,
transport: transports[chain.id]!,
transport: (transports as Record<number, Transport>)[chain.id]!,
})
},
get delegation() {
Expand All @@ -82,7 +94,7 @@ export function create(

const emitter = Provider.createEmitter()

function setupSubscriptions() {
function setup() {
const unsubscribe_accounts = store.subscribe(
(state) => state.accounts,
(accounts) => {
Expand All @@ -100,12 +112,14 @@ export function create(
},
)

if (keystoreHost) setupWebAuthnOrigin({ rpId: keystoreHost })

return () => {
unsubscribe_accounts()
unsubscribe_chain()
}
}
const unsubscribe = setupSubscriptions()
const destroy = setup()

const provider = Provider.from({
...emitter,
Expand All @@ -124,7 +138,9 @@ export function create(
case 'eth_requestAccounts': {
if (!headless) throw new Provider.UnsupportedMethodError()

const { account } = await AccountDelegation.load(state.client)
const { account } = await AccountDelegation.load(state.client, {
rpId: keystoreHost,
})

store.setState((x) => ({ ...x, accounts: [account] }))

Expand Down Expand Up @@ -176,7 +192,7 @@ export function create(
const { account } = await AccountDelegation.create(state.client, {
delegation: state.delegation,
label,
rpId: webauthn?.rpId,
rpId: keystoreHost,
})

store.setState((x) => ({ ...x, accounts: [account] }))
Expand Down Expand Up @@ -304,7 +320,7 @@ export function create(
},
destroy() {
emitter.removeAllListeners()
unsubscribe()
destroy()
},
provider,
_internal: {
Expand All @@ -321,31 +337,29 @@ export namespace create {
],
> = {
/** List of supported chains. */
chains: chains | readonly [Chains.Chain, ...Chains.Chain[]]
chains?: chains | readonly [Chains.Chain, ...Chains.Chain[]]
/** Transport to use for each chain. */
transports: Record<chains[number]['id'], Transport>
transports?: Record<chains[number]['id'], Transport>
} & (
| {
/**
* Whether to run EIP-1193 Provider in headless mode.
* @default true
*/
headless: true
headless?: true | undefined
/** WebAuthn configuration. */
webauthn?:
| {
rpId?: string | undefined
}
| undefined
keystoreHost?: string | undefined
}
| {
headless?: false | undefined
webauthn?: undefined
keystoreHost?: undefined
}
)

export const defaultParameters = {
chains: [Chains.odysseyTestnet],
headless: true,
keystoreHost: 'oddworld-tau.vercel.app',
transports: {
[Chains.odysseyTestnet.id]: http(),
},
Expand Down Expand Up @@ -400,6 +414,30 @@ function requireParameter(
})
}

function setupWebAuthnOrigin(parameters: {
rpId: string
}) {
if (typeof window === 'undefined') return

const { rpId } = parameters
const origin = `${window.location.protocol}//${window.location.hostname}`
const url = `https://${rpId}/.well-known/webauthn`
fetch(url)
.then((x) => x.json())
.then((x) => {
if (x.origins.includes(origin)) return
fetch(`https://${rpId}/.well-known/webauthn`, {
method: 'PATCH',
body: JSON.stringify({
origin: `${window.location.protocol}//${window.location.hostname}`,
}),
headers: {
'Content-Type': 'application/json',
},
})
})
}

const storage = {
getItem(name) {
const value = localStorage.getItem(name)
Expand Down
13 changes: 13 additions & 0 deletions src/internal/accountDelegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export async function createWebAuthnKey(
parameters: createWebAuthnKey.Parameters,
): Promise<WebAuthnKey> {
const { expiry = 0n, rpId, label, userId } = parameters

const key = await WebAuthnP256.createCredential({
authenticatorSelection: {
requireResidentKey: false,
Expand All @@ -250,6 +251,7 @@ export async function createWebAuthnKey(
id: userId,
},
})

return {
...key,
expiry,
Expand Down Expand Up @@ -356,11 +358,15 @@ export declare namespace execute {
/** Loads an existing Account. */
export async function load<chain extends Chain | undefined>(
client: Client<Transport, chain>,
parameters: load.Parameters = {},
) {
const { rpId } = parameters

// We will sign a random challenge. We need to do this to extract the
// user id (ie. the address) to query for the Account's keys.
const { raw } = await WebAuthnP256.sign({
challenge: '0x',
rpId,
})

const response = raw.response as AuthenticatorAssertionResponse
Expand Down Expand Up @@ -408,6 +414,13 @@ export async function load<chain extends Chain | undefined>(
}
}

export declare namespace load {
type Parameters = {
/** Relying Party ID. */
rpId?: string | undefined
}
}

/** Signs a payload with a key on the Account. */
export async function sign(parameters: sign.Parameters) {
const { account, payload, keyIndex } = parameters
Expand Down

0 comments on commit cf4fa68

Please sign in to comment.