From f593f425a0c19af26415deb6bcdef4d112d9c184 Mon Sep 17 00:00:00 2001 From: Marco Lipparini Date: Sun, 24 Mar 2024 20:30:49 +0100 Subject: [PATCH] Adding support for token scopes --- src/contexts/Web3AuthContext.tsx | 7 ++-- src/lib/jwt.ts | 55 +++++++++++++++++++++----------- src/nextjs/route-handlers.ts | 2 +- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/contexts/Web3AuthContext.tsx b/src/contexts/Web3AuthContext.tsx index 03600dc..b98245c 100644 --- a/src/contexts/Web3AuthContext.tsx +++ b/src/contexts/Web3AuthContext.tsx @@ -42,7 +42,7 @@ const storeAuthorizedAddress = (address: Address) => { }; interface Web3AuthContext { - signJwtToken: (expiration: number) => Promise; + signJwtToken: (expiration: number, scopes: [string, ...string[]]) => Promise; clearJwtToken: () => void; activeJwtToken: string | null; authorizedAddress?: Address; @@ -79,7 +79,7 @@ export function Web3AuthProvider({ }, [jwtTokenStorageKeySuffix, walletClient]); const signJwtToken = useCallback( - async (expiration: number) => { + async (expiration: number, scopes: [string, ...string[]]) => { if (localStorageKey === null || walletClient === undefined) { throw new Web3AuthError('Failed creating authentication token. Wallet not ready yet!'); } @@ -88,6 +88,7 @@ export function Web3AuthProvider({ message: signatureMessage, address: authorizedAddress ?? walletClient.account.address, signer: walletClient, + scopes, expiration, }); @@ -123,7 +124,7 @@ export function Web3AuthProvider({ maxAllowedExpiration: jwtTokenMaxValidity === undefined ? undefined : Date.now() + jwtTokenMaxValidity, }); } catch (e: any) { - if (e.codes?.includes(W3A_ERROR_JWT_DECODING)) { + if (e.codes?.includes(W3A_ERROR_JWT_DECODING) !== true) { throw e; } } diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts index 78663b2..632c922 100644 --- a/src/lib/jwt.ts +++ b/src/lib/jwt.ts @@ -1,4 +1,4 @@ -import { Account, Address, Hex, PublicClient, WalletClient, getAddress, isAddressEqual, zeroAddress } from 'viem'; +import { Account, Address, Hex, PublicClient, WalletClient, getAddress, isAddressEqual } from 'viem'; import { verifySignatureWithDelegation } from './signatures'; import { CacheController } from './jwt-cache'; @@ -33,6 +33,7 @@ export type AuthJwtPayload = { chainId: number; walletAddress: Address; signerAddress: Address; + scopes: [string, ...string[]]; createdAt: number; exp: number; }; @@ -49,34 +50,40 @@ export type JwtVerificationResult = { walletAddress: Address; signerAddress: Address; isDelegated: boolean; + scopes: [string, ...string[]]; } | null; const buildMessage = ({ message, payload }: { message: string; payload: AuthJwtPayload }) => - `${message}\n\n[Auth Token Validity Info]\nStart: ${new Date(payload.createdAt).toUTCString()}\nEnd: ${new Date( - payload.exp, - ).toUTCString()}\nAddress: ${payload.walletAddress}\nSigner: ${payload.signerAddress}\nChain ID: ${payload.chainId}`; + `${message}\n\n[Auth Token Details]\nScopes:\n${payload.scopes.map((scope) => ` - ${scope}\n`).join('')}Start: ${new Date( + payload.createdAt, + ).toUTCString()}\nEnd: ${new Date(payload.exp).toUTCString()}\nAddress: ${payload.walletAddress}\nSigner: ${ + payload.signerAddress + }\nChain ID: ${payload.chainId}`; const buildPayload = ({ chainId, address, signer, + scopes, exp, }: { chainId: number; address: Address; signer: Address; + scopes: [string, ...string[]]; exp: number; }): AuthJwtPayload => { const createdAt = Date.now(); if (createdAt > exp) { - throw new Web3AuthError('Expiration timestamp must be in the future.'); + throw new Web3AuthError('Expiration timestamp must be in the future'); } return { chainId, walletAddress: getAddress(address), signerAddress: getAddress(signer), + scopes, createdAt, exp, }; @@ -98,21 +105,28 @@ export const createJwtToken = async ({ message, address, signer, + scopes, expiration, }: { message: string; address: Address; signer: Signer; + scopes: [string, ...string[]]; expiration: number; }): Promise => { if (signer.account === undefined) { - throw new Web3AuthError('Invalid web3 client: an account is required in order to sign a token.'); + throw new Web3AuthError('Invalid web3 client: an account is required in order to sign a token'); + } + + if (scopes.length < 1) { + throw new Web3AuthError('JWT tokens must have at least one scope'); } const payload = buildPayload({ chainId: await signer.getChainId(), address, signer: signer.account.address, + scopes, exp: expiration, }); const signatureMessage = buildMessage({ message, payload }); @@ -149,6 +163,7 @@ export const verifyJwtToken = async ({ walletAddress: payload.walletAddress, signerAddress: payload.signerAddress, isDelegated: !isAddressEqual(payload.walletAddress, payload.signerAddress), + scopes: payload.scopes, }; // Check cache @@ -171,19 +186,21 @@ export const verifyJwtToken = async ({ return null; } - if ( - !isAddressEqual( - (await verifySignatureWithDelegation({ - walletAddress: payload.walletAddress, - signerAddress: payload.signerAddress, - message: buildMessage({ message, payload }), - signature, - web3Client, - delegateXyzRights, - })) ?? zeroAddress, - payload.walletAddress, - ) - ) { + // Is there at least one scope? + if (payload.scopes.length < 1) { + return null; + } + + const verifiedAddress = await verifySignatureWithDelegation({ + walletAddress: payload.walletAddress, + signerAddress: payload.signerAddress, + message: buildMessage({ message, payload }), + signature, + web3Client, + delegateXyzRights, + }); + + if (verifiedAddress === null || !isAddressEqual(verifiedAddress, payload.walletAddress)) { return null; } diff --git a/src/nextjs/route-handlers.ts b/src/nextjs/route-handlers.ts index 2fba75f..f7e55a0 100644 --- a/src/nextjs/route-handlers.ts +++ b/src/nextjs/route-handlers.ts @@ -43,7 +43,7 @@ export const createWeb3UserFetcher = ({ return verificationResult; } catch (e: any) { - if (e.codes?.includes(W3A_ERROR_JWT_DECODING)) { + if (e.codes?.includes(W3A_ERROR_JWT_DECODING) === true) { throw new HandlerError({ title: e.message, status: 400 }); }