Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for token scopes #1

Merged
merged 1 commit into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/contexts/Web3AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const storeAuthorizedAddress = (address: Address) => {
};

interface Web3AuthContext {
signJwtToken: (expiration: number) => Promise<void>;
signJwtToken: (expiration: number, scopes: [string, ...string[]]) => Promise<void>;
clearJwtToken: () => void;
activeJwtToken: string | null;
authorizedAddress?: Address;
Expand Down Expand Up @@ -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!');
}
Expand All @@ -88,6 +88,7 @@ export function Web3AuthProvider({
message: signatureMessage,
address: authorizedAddress ?? walletClient.account.address,
signer: walletClient,
scopes,
expiration,
});

Expand Down Expand Up @@ -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;
}
}
Expand Down
55 changes: 36 additions & 19 deletions src/lib/jwt.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -33,6 +33,7 @@ export type AuthJwtPayload = {
chainId: number;
walletAddress: Address;
signerAddress: Address;
scopes: [string, ...string[]];
createdAt: number;
exp: number;
};
Expand All @@ -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,
};
Expand All @@ -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<string> => {
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 });
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/nextjs/route-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand Down