[Auth] complete auth update & restructure (#460)

This commit is contained in:
Adam Majmudar
2023-01-27 17:06:58 -05:00
committed by GitHub
parent fdb9baedee
commit a6c074c3f3
161 changed files with 5873 additions and 3244 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/wallets": minor
---
Wallets abstraction along with major Auth upgrade. Wallets split into EVM and Solana entrypoints along with `GenericAuthWallet` implementation.
+30
View File
@@ -0,0 +1,30 @@
---
"@thirdweb-dev/auth": major
---
Complete Auth redesign and update to add a number of major and quality of life improvements, including the following:
- Ability to use Auth APIs with both cookies and JWTs, allowing non browser clients to interact with Auth (mobile, gaming, scripts, etc.)
- Ability to store session data and other data on the Auth user
- Callbacks to run side-effects on login, logout, and requesting user data
- Ability to configure cookies for custom domains and backend setups
- Support for validation of the entire EIP4361/CAIP122 specification
- No more need for redirects or payload encoding on Auth requests
- and more...
See the new documentation to view the new changes and usage: [Auth Documentation](https://portal.thirdweb.com/auth).
## How to upgrade
The `ThirdwebAuth` constructor now takes the `domain` in the constructor, and takes a more generic `wallet` interface as input. The `wallet` can be imported from the `@thirdweb-dev/wallets` package, or for more simpler use cases, from the `@thirdweb-dev/auth/evm` and `@thirdweb-dev/auth/solana` entrypoints.
```js
import { PrivateKeyWallet } from "@thirdweb-dev/auth/evm";
// Pass in domain and wallet to the constructor
const wallet = new PrivateKeyWallet("0x...");
const auth = new ThirdwebAuth(wallet, "example.com");
// Auth functions no longer require domain to be passed in
const payload = await auth.login();
```
+49
View File
@@ -0,0 +1,49 @@
---
"@thirdweb-dev/react-core": minor
---
All Auth hooks and configuration have been upgraded along with the major upgrade to Auth. This includes changes in necessary `authConfig` to the `ThirdwebProvider`, as well as usage of the `useLogin`, `useLogout`, and `useUser` hooks.
## How to Upgrade
In order to upgrade your frontend setup to account for these changes, you'll need to make the following changes to your app:
**1. Remove `loginRedirect` from `authConfig`**
In your `ThirdwebProvider`, you can remove the `loginRedirect` option from the `authConfig` object, as the `login` endpoint no longer uses redirects.
```jsx
export default function MyApp({ Component, pageProps }) {
return (
<ThirdwebProvider
authConfig={{
domain: "example.com",
authUrl: "/api/auth",
// No more loginRedirect
}}
>
<Component {...pageProps} />
</ThirdwebProvider>
);
}
```
**2. Update `useLogin` and `useLogout` to use object destructuring**
The `useLogin` and `useLogout` hooks now return an object with a `login` and `logout` function (as well as `isLoading` states), respectively. You'll need to update your usage of these hooks to use object destructuring.
```jsx
import { useLogin, useLogout } from "@thirdweb-dev/react-core";
export default function Component() {
const { login } = useLogin();
const { logout } = useLogout();
return (
<div>
<button onClick={login}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
}
```
+5
View File
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/unity-js-bridge": minor
---
Upgrade with new wallets for major Auth upgrade
+49
View File
@@ -0,0 +1,49 @@
---
"@thirdweb-dev/react": minor
---
All Auth hooks and configuration have been upgraded along with the major upgrade to Auth. This includes changes in necessary `authConfig` to the `ThirdwebProvider`, as well as usage of the `useLogin`, `useLogout`, and `useUser` hooks.
## How to upgrade
In order to upgrade your frontend setup to account for these changes, you'll need to make the following changes to your app:
**1. Remove `loginRedirect` from `authConfig`**
In your `ThirdwebProvider`, you can remove the `loginRedirect` option from the `authConfig` object, as the `login` endpoint no longer uses redirects.
```jsx
export default function MyApp({ Component, pageProps }) {
return (
<ThirdwebProvider
authConfig={{
domain: "example.com",
authUrl: "/api/auth",
// No more loginRedirect
}}
>
<Component {...pageProps} />
</ThirdwebProvider>
);
}
```
**2. Update `useLogin` and `useLogout` to use object destructuring**
The `useLogin` and `useLogout` hooks now return an object with a `login` and `logout` function (as well as `isLoading` states), respectively. You'll need to update your usage of these hooks to use object destructuring.
```jsx
import { useLogin, useLogout } from "@thirdweb-dev/react";
export default function Component() {
const { login } = useLogin();
const { logout } = useLogout();
return (
<div>
<button onClick={login}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
}
```
+4
View File
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-auth-evm.cjs.js",
"module": "dist/thirdweb-dev-auth-evm.esm.js"
}
-4
View File
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-auth-next-evm.cjs.js",
"module": "dist/thirdweb-dev-auth-next-evm.esm.js"
}
-4
View File
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-auth-next-solana.cjs.js",
"module": "dist/thirdweb-dev-auth-next-solana.esm.js"
}
+59 -22
View File
@@ -1,52 +1,54 @@
{
"name": "@thirdweb-dev/auth",
"version": "2.0.41",
"main": "dist/thirdweb-dev-auth.cjs.js",
"module": "dist/thirdweb-dev-auth.esm.js",
"exports": {
".": {
"module": "./dist/thirdweb-dev-auth.esm.js",
"default": "./dist/thirdweb-dev-auth.cjs.js"
},
"./evm": {
"module": "./evm/dist/thirdweb-dev-auth-evm.esm.js",
"default": "./evm/dist/thirdweb-dev-auth-evm.cjs.js"
},
"./next": {
"module": "./next/dist/thirdweb-dev-auth-next.esm.js",
"default": "./next/dist/thirdweb-dev-auth-next.cjs.js"
},
"./solana": {
"module": "./solana/dist/thirdweb-dev-auth-solana.esm.js",
"default": "./solana/dist/thirdweb-dev-auth-solana.cjs.js"
},
"./express": {
"module": "./express/dist/thirdweb-dev-auth-express.esm.js",
"default": "./express/dist/thirdweb-dev-auth-express.cjs.js"
},
"./next/evm": {
"module": "./next/evm/dist/thirdweb-dev-auth-next-evm.esm.js",
"default": "./next/evm/dist/thirdweb-dev-auth-next-evm.cjs.js"
},
"./next-auth": {
"module": "./next-auth/dist/thirdweb-dev-auth-next-auth.esm.js",
"default": "./next-auth/dist/thirdweb-dev-auth-next-auth.cjs.js"
},
"./next/solana": {
"module": "./next/solana/dist/thirdweb-dev-auth-next-solana.esm.js",
"default": "./next/solana/dist/thirdweb-dev-auth-next-solana.cjs.js"
},
"./package.json": "./package.json"
},
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/auth",
"author": "thirdweb eng <[email protected]>",
"license": "Apache-2.0",
"files": [
"dist/**/*",
"next/**/*",
"express/**/*",
"next-auth/**/*"
],
"scripts": {
"build": "preconstruct build",
"format": "prettier --write 'src/**/*'",
"lint": "eslint src/",
"fix": "eslint src/ --fix",
"clean": "rm -rf dist/ && rm -rf node_modules/"
"clean": "rm -rf dist/ && rm -rf node_modules/",
"test": "mocha --config './test/.mocharc.json' --timeout 30000 --parallel './test/**/*.test.ts'"
},
"preconstruct": {
"entrypoints": [
"next/index.ts",
"next/evm/index.ts",
"next/solana/index.ts",
"index.ts",
"express/index.ts",
"next-auth/index.ts"
"next/index.ts",
"next-auth/index.ts",
"evm/index.ts",
"solana/index.ts"
],
"___experimentalFlags_WILL_CHANGE_IN_PATCH": {
"exports": true
@@ -61,12 +63,21 @@
"@microsoft/api-extractor": "^7.29.2",
"@microsoft/tsdoc": "^0.14.1",
"@preconstruct/cli": "^2.2.1",
"@solana/web3.js": "^1.73.0",
"@swc-node/register": "^1.5.4",
"@swc/core": "^1.3.23",
"@thirdweb-dev/sdk": "*",
"@thirdweb-dev/wallets": "*",
"@types/bs58": "^4.0.1",
"@types/chai": "^4.3.4",
"@types/cookie": "^0.5.1",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"@wagmi/core": "^0.8.18",
"chai": "^4.3.7",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.26.0",
@@ -75,6 +86,7 @@
"eslint-plugin-tsdoc": "^0.2.16",
"ethers": "^5.7.2",
"express": "^4.18.1",
"mocha": "^10.2.0",
"next": "^12.2.0",
"next-auth": "^4.10.3",
"prettier": "^2.7.1",
@@ -83,13 +95,36 @@
"typescript": "^4.7.4"
},
"peerDependencies": {
"@noble/ed25519": "^1.7.1",
"@solana/web3.js": "^1.73.0",
"@thirdweb-dev/sdk": "*",
"bs58": "^5.0.0",
"cookie-parser": "^1.4.6",
"ethers": ">=5.5.1",
"express": "^4.18.1",
"next": "^12.2.0",
"next-auth": "^4.10.3"
"next-auth": "^4.10.3",
"tweetnacl": "^1.0.3"
},
"peerDependenciesMeta": {
"@noble/ed25519": {
"optional": true
},
"@solana/web3.js": {
"optional": true
},
"bs58": {
"optional": true
},
"cookie-parser": {
"optional": true
},
"tweetnacl": {
"optional": true
},
"ethers": {
"optional": true
},
"express": {
"optional": true
},
@@ -102,6 +137,8 @@
},
"dependencies": {
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6"
"uuid": "^9.0.0",
"yarn": "^1.22.19",
"zod": "^3.20.2"
}
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-auth-solana.cjs.js",
"module": "dist/thirdweb-dev-auth-solana.esm.js"
}
+412
View File
@@ -0,0 +1,412 @@
import {
LoginOptions,
LoginPayload,
GenerateOptions,
LoginPayloadData,
LoginPayloadDataSchema,
AuthenticationPayloadDataSchema,
AuthenticationPayloadData,
LoginOptionsSchema,
VerifyOptionsSchema,
VerifyOptions,
GenerateOptionsSchema,
AuthenticateOptionsSchema,
AuthenticateOptions,
User,
Json,
} from "./schema";
import { isBrowser } from "./utils";
import type { GenericAuthWallet } from "@thirdweb-dev/wallets";
export class ThirdwebAuth {
private domain: string;
private wallet: GenericAuthWallet;
constructor(wallet: GenericAuthWallet, domain: string) {
this.wallet = wallet;
this.domain = domain;
}
public updateWallet(wallet: GenericAuthWallet) {
this.wallet = wallet;
}
public async login(options?: LoginOptions): Promise<LoginPayload> {
const parsedOptions = LoginOptionsSchema.parse(options);
let chainId: string | undefined = parsedOptions?.chainId;
if (!chainId && this.wallet.getChainId) {
try {
chainId = (await this.wallet.getChainId()).toString();
} catch {
// ignore error
}
}
const payloadData = LoginPayloadDataSchema.parse({
type: this.wallet.type,
domain: parsedOptions?.domain || this.domain,
address: await this.wallet.getAddress(),
statement: parsedOptions?.statement,
version: parsedOptions?.version,
uri:
parsedOptions?.uri || (isBrowser() ? window.location.href : undefined),
chain_id: chainId,
nonce: parsedOptions?.nonce,
expiration_time:
parsedOptions?.expirationTime || new Date(Date.now() + 1000 * 60 * 5),
invalid_before: parsedOptions?.invalidBefore,
resources: parsedOptions?.resources,
});
const message = this.generateMessage(payloadData);
const signature = await this.wallet.signMessage(message);
return {
payload: payloadData,
signature,
};
}
public async verify(
payload: LoginPayload,
options?: VerifyOptions,
): Promise<string> {
const parsedOptions = VerifyOptionsSchema.parse(options);
if (payload.payload.type !== this.wallet.type) {
throw new Error(
`Expected chain type '${this.wallet.type}' does not match chain type on payload '${payload.payload.type}'`,
);
}
// Check that the intended domain matches the domain of the payload
const domain = parsedOptions?.domain || this.domain;
if (payload.payload.domain !== domain) {
throw new Error(
`Expected domain '${domain}' does not match domain on payload '${payload.payload.domain}'`,
);
}
// Check that the payload statement matches the expected statement
if (parsedOptions?.statement) {
if (payload.payload.statement !== parsedOptions.statement) {
throw new Error(
`Expected statement '${parsedOptions.statement}' does not match statement on payload '${payload.payload.statement}'`,
);
}
}
// Check that the intended URI matches the URI of the payload
if (parsedOptions?.uri) {
if (payload.payload.uri !== parsedOptions.uri) {
throw new Error(
`Expected URI '${parsedOptions.uri}' does not match URI on payload '${payload.payload.uri}'`,
);
}
}
// Check that the intended version matches the version of the payload
if (parsedOptions?.version) {
if (payload.payload.version !== parsedOptions.version) {
throw new Error(
`Expected version '${parsedOptions.version}' does not match version on payload '${payload.payload.version}'`,
);
}
}
// Check that the intended chain ID matches the chain ID of the payload
if (parsedOptions?.chainId) {
if (payload.payload.chain_id !== parsedOptions.chainId) {
throw new Error(
`Expected chain ID '${parsedOptions.chainId}' does not match chain ID on payload '${payload.payload.chain_id}'`,
);
}
}
// Check that the payload nonce is valid
if (parsedOptions?.validateNonce !== undefined) {
try {
await parsedOptions.validateNonce(payload.payload.nonce);
} catch (err) {
console.log(err);
throw new Error(`Login request nonce is invalid`);
}
}
// Check that it isn't before the invalid before time
const currentTime = new Date();
if (currentTime < new Date(payload.payload.invalid_before)) {
throw new Error(`Login request is not yet valid`);
}
// Check that the payload hasn't expired
if (currentTime > new Date(payload.payload.expiration_time)) {
throw new Error(`Login request has expired`);
}
// Check that the specified resources are present on the payload
if (parsedOptions?.resources) {
const missingResources = parsedOptions.resources.filter(
(resource) => !payload.payload.resources?.includes(resource),
);
if (missingResources.length > 0) {
throw new Error(
`Login request is missing required resources: ${missingResources.join(
", ",
)}`,
);
}
}
// Check that the signing address is the claimed wallet address
const message = this.generateMessage(payload.payload);
const chainId =
this.wallet.type === "evm" && payload.payload.chain_id
? parseInt(payload.payload.chain_id)
: undefined;
const verified = await this.verifySignature(
message,
payload.signature,
payload.payload.address,
chainId,
);
if (!verified) {
throw new Error(
`Signer address does not match payload address '${payload.payload.address.toLowerCase()}'`,
);
}
return payload.payload.address;
}
public async generate(
payload: LoginPayload,
options?: GenerateOptions,
): Promise<string> {
if (isBrowser()) {
throw new Error(
"Authentication tokens should not be generated in the browser, as they must be signed by a server-side admin wallet.",
);
}
const parsedOptions = GenerateOptionsSchema.parse(options);
const domain = parsedOptions?.domain || this.domain;
const userAddress = await this.verify(payload, {
domain,
...parsedOptions?.verifyOptions,
});
let session: Json | undefined = undefined;
if (typeof parsedOptions?.session === "function") {
const sessionTrigger = (await parsedOptions.session(userAddress)) as Json;
if (sessionTrigger) {
session = sessionTrigger;
}
} else {
session = parsedOptions?.session;
}
const adminAddress = await this.wallet.getAddress();
const payloadData = AuthenticationPayloadDataSchema.parse({
iss: adminAddress,
sub: userAddress,
aud: domain,
nbf: parsedOptions?.invalidBefore || new Date(),
exp:
parsedOptions?.expirationTime ||
new Date(Date.now() + 1000 * 60 * 60 * 5),
iat: new Date(),
jti: parsedOptions?.tokenId,
ctx: session,
});
const message = JSON.stringify(payloadData);
const signature = await this.wallet.signMessage(message);
// Header used for JWT token specifying hash algorithm
const header = {
// Specify ECDSA with SHA-256 for hashing algorithm
alg: "ES256",
typ: "JWT",
};
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
"base64",
);
const encodedData = Buffer.from(JSON.stringify(payloadData))
.toString("base64")
.replace(/=/g, "");
const encodedSignature = Buffer.from(signature).toString("base64");
// Generate a JWT token with base64 encoded header, payload, and signature
const token = `${encodedHeader}.${encodedData}.${encodedSignature}`;
return token;
}
/**
* Authenticate With Token
* @remarks Server-side function that authenticates the provided JWT token. This function verifies that
* the provided authentication token is valid and returns the address of the authenticated wallet.
*
* @param domain - The domain of the server-side application doing authentication
* @param token - The authentication token being used
* @returns The address of the authenticated wallet
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
* const token = await sdk.auth.generateAuthToken(domain, loginPayload);
*
* // Authenticate the token and get the address of authenticating users wallet
* const address = sdk.auth.authenticate(domain, token);
* ```
*/
public async authenticate<TSession extends Json = Json>(
token: string,
options?: AuthenticateOptions,
): Promise<User<TSession>> {
if (isBrowser()) {
throw new Error(
"Should not authenticate tokens in the browser, as they must be verified by the server-side admin wallet.",
);
}
const parsedOptions = AuthenticateOptionsSchema.parse(options);
const domain = parsedOptions?.domain || this.domain;
const encodedPayload = token.split(".")[1];
const encodedSignature = token.split(".")[2];
const payload: AuthenticationPayloadData = JSON.parse(
Buffer.from(encodedPayload, "base64").toString(),
);
const signature = Buffer.from(encodedSignature, "base64").toString();
// Check that the payload unique ID is valid
if (parsedOptions?.validateTokenId !== undefined) {
try {
await parsedOptions.validateTokenId(payload.jti);
} catch (err) {
throw new Error(`Token ID is invalid`);
}
}
// Check that the token audience matches the domain
if (payload.aud !== domain) {
throw new Error(
`Expected token to be for the domain '${domain}', but found token with domain '${payload.aud}'`,
);
}
// Check that the token is past the invalid before time
const currentTime = Math.floor(new Date().getTime() / 1000);
if (currentTime < payload.nbf) {
throw new Error(
`This token is invalid before epoch time '${payload.nbf}', current epoch time is '${currentTime}'`,
);
}
// Check that the token hasn't expired
if (currentTime > payload.exp) {
throw new Error(
`This token expired at epoch time '${payload.exp}', current epoch time is '${currentTime}'`,
);
}
// Check that the connected wallet matches the token issuer
const connectedAddress = await this.wallet.getAddress();
if (connectedAddress.toLowerCase() !== payload.iss.toLowerCase()) {
throw new Error(
`Expected the connected wallet address '${connectedAddress}' to match the token issuer address '${payload.iss}'`,
);
}
let chainId: number | undefined = undefined;
if (this.wallet.getChainId) {
try {
chainId = await this.wallet.getChainId();
} catch {
// ignore error
}
}
const verified = await this.verifySignature(
JSON.stringify(payload),
signature,
connectedAddress,
chainId,
);
if (!verified) {
throw new Error(
`The connected wallet address '${connectedAddress}' did not sign the token`,
);
}
return {
address: payload.sub,
session: payload.ctx as TSession | undefined,
};
}
private async verifySignature(
message: string,
signature: string,
address: string,
chainId?: number,
) {
return this.wallet.verifySignature(message, signature, address, chainId);
}
/**
* Generates a EIP-4361 & CAIP-122 compliant message to sign based on the login payload
*/
private generateMessage(payload: LoginPayloadData): string {
const typeField = payload.type === "evm" ? "Ethereum" : "Solana";
const header = `${payload.domain} wants you to sign in with your ${typeField} account:`;
let prefix = [header, payload.address].join("\n");
prefix = [prefix, payload.statement].join("\n\n");
if (payload.statement) {
prefix += "\n";
}
const suffixArray = [];
if (payload.uri) {
const uriField = `URI: ${payload.uri}`;
suffixArray.push(uriField);
}
const versionField = `Version: ${payload.version}`;
suffixArray.push(versionField);
if (payload.chain_id) {
const chainField = `Chain ID: ` + payload.chain_id || "1";
suffixArray.push(chainField);
}
const nonceField = `Nonce: ${payload.nonce}`;
suffixArray.push(nonceField);
const issuedAtField = `Issued At: ${payload.issued_at}`;
suffixArray.push(issuedAtField);
const expiryField = `Expiration Time: ${payload.expiration_time}`;
suffixArray.push(expiryField);
if (payload.invalid_before) {
const invalidBeforeField = `Not Before: ${payload.invalid_before}`;
suffixArray.push(invalidBeforeField);
}
if (payload.resources) {
suffixArray.push(
[`Resources:`, ...payload.resources.map((x) => `- ${x}`)].join("\n"),
);
}
const suffix = suffixArray.join("\n");
return [prefix, suffix].join("\n");
}
}
+2
View File
@@ -0,0 +1,2 @@
export { ThirdwebAuth } from "./auth";
export * from "./schema";
+208
View File
@@ -0,0 +1,208 @@
import { utils, BigNumber } from "ethers";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
export const AddressSchema = z.string().refine(
(arg) => utils.isAddress(arg),
(out) => {
return {
message: `${out} is not a valid address`,
};
},
);
export const RawDateSchema = z.date().transform((i) => {
return BigNumber.from(Math.floor(i.getTime() / 1000));
});
export const AccountTypeSchema = z.union([
z.literal("evm"),
z.literal("solana"),
]);
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
export type Json = Literal | { [key: string]: Json } | Json[];
const JsonSchema: z.ZodType<Json> = z.lazy(
() => z.union([literalSchema, z.array(JsonSchema), z.record(JsonSchema)]),
{ invalid_type_error: "Provided value was not valid JSON" },
);
/**
* @internal
*/
export const LoginOptionsSchema = z
.object({
domain: z.string().optional(),
statement: z.string().optional(),
uri: z.string().optional(),
version: z.string().optional(),
chainId: z.string().optional(),
nonce: z.string().optional(),
expirationTime: z.date().optional(),
invalidBefore: z.date().optional(),
resources: z.array(z.string()).optional(),
})
.optional();
/**
* @internal
*/
export const LoginPayloadDataSchema = z.object({
type: AccountTypeSchema,
domain: z.string(),
address: z.string(),
statement: z
.string()
.default(
"Please ensure that the domain above matches the URL of the current website.",
),
uri: z.string().optional(),
version: z.string().default("1"),
chain_id: z.string().optional(),
nonce: z.string().default(uuidv4()),
issued_at: z
.date()
.default(new Date())
.transform((d) => d.toISOString()),
expiration_time: z.date().transform((d) => d.toISOString()),
invalid_before: z
.date()
.default(new Date())
.transform((d) => d.toISOString()),
resources: z.array(z.string()).optional(),
});
/**
* @internal
*/
export const LoginPayloadSchema = z.object({
payload: LoginPayloadDataSchema,
signature: z.string(),
});
/**
* @internal
*/
const VerifyOptionsSchemaRequired = z.object({
domain: z.string().optional(),
statement: z.string().optional(),
uri: z.string().optional(),
version: z.string().optional(),
chainId: z.string().optional(),
validateNonce: z.function().args(z.string()).optional(),
resources: z.array(z.string()).optional(),
});
/**
* @internal
*/
export const VerifyOptionsSchema = VerifyOptionsSchemaRequired.optional();
/**
* @internal
*/
export const GenerateOptionsSchema = z
.object({
domain: z.string().optional(),
tokenId: z.string().optional(),
expirationTime: z.date().optional(),
invalidBefore: z.date().optional(),
session: z.union([JsonSchema, z.function().args(z.string())]).optional(),
verifyOptions: VerifyOptionsSchemaRequired.omit({
domain: true,
}).optional(),
})
.optional();
/**
* @internal
*/
export const AuthenticationPayloadDataSchema = z.object({
iss: z.string(),
sub: z.string(),
aud: z.string(),
exp: RawDateSchema.transform((b) => b.toNumber()),
nbf: RawDateSchema.transform((b) => b.toNumber()),
iat: RawDateSchema.transform((b) => b.toNumber()),
jti: z.string().default(uuidv4()),
ctx: JsonSchema.optional(),
});
/**
* @internal
*/
export const AuthenticationPayloadSchema = z.object({
payload: AuthenticationPayloadDataSchema,
signature: z.string(),
});
/**
* @internal
*/
export const AuthenticateOptionsSchema = z
.object({
domain: z.string().optional(),
validateTokenId: z.function().args(z.string()).optional(),
})
.optional();
/**
* @public
*/
export type LoginOptions = z.input<typeof LoginOptionsSchema>;
/**
* @public
*/
export type LoginPayloadData = z.output<typeof LoginPayloadDataSchema>;
/**
* @public
*/
export type LoginPayload = z.output<typeof LoginPayloadSchema>;
/**
* @public
*/
export type VerifyOptions = z.input<typeof VerifyOptionsSchema>;
/**
* @public
*/
export type GenerateOptions = z.input<typeof GenerateOptionsSchema>;
/**
* @public
*/
export type AuthenticationPayloadData = z.output<
typeof AuthenticationPayloadDataSchema
>;
/**
* @public
*/
export type AuthenticationPayload = z.output<
typeof AuthenticationPayloadSchema
>;
/**
* @public
*/
export type AuthenticateOptions = z.output<typeof AuthenticateOptionsSchema>;
/**
* @public
*/
export type User<TContext extends Json = Json> = {
address: string;
session?: TContext;
};
export const LoginPayloadOutputSchema = LoginPayloadSchema.extend({
payload: LoginPayloadDataSchema.extend({
issued_at: z.string(),
expiration_time: z.string(),
invalid_before: z.string(),
}),
});
+9
View File
@@ -0,0 +1,9 @@
/**
* @internal
*/
export const isBrowser = () => typeof window !== "undefined";
/**
* @internal
*/
export const isNode = () => !isBrowser();
+613
View File
@@ -0,0 +1,613 @@
import type { Chain } from "@wagmi/core";
const arbitrum: Chain = {
id: 42_161,
name: "Arbitrum One",
network: "arbitrum",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
alchemy: {
http: ["https://arb-mainnet.g.alchemy.com/v2"],
webSocket: ["wss://arb-mainnet.g.alchemy.com/v2"],
},
infura: {
http: ["https://arbitrum-mainnet.infura.io/v3"],
webSocket: ["wss://arbitrum-mainnet.infura.io/ws/v3"],
},
default: {
http: ["https://arb1.arbitrum.io/rpc"],
},
public: {
http: ["https://arb1.arbitrum.io/rpc"],
},
},
blockExplorers: {
etherscan: { name: "Arbiscan", url: "https://arbiscan.io" },
default: { name: "Arbiscan", url: "https://arbiscan.io" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 7654707,
},
},
};
const arbitrumGoerli: Chain = {
id: 421_613,
name: "Arbitrum Goerli",
network: "arbitrum-goerli",
nativeCurrency: {
name: "Arbitrum Goerli Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
alchemy: {
http: ["https://arb-goerli.g.alchemy.com/v2"],
webSocket: ["wss://arb-goerli.g.alchemy.com/v2"],
},
infura: {
http: ["https://arbitrum-goerli.infura.io/v3"],
webSocket: ["wss://arbitrum-goerli.infura.io/ws/v3"],
},
default: {
http: ["https://goerli-rollup.arbitrum.io/rpc"],
},
public: {
http: ["https://goerli-rollup.arbitrum.io/rpc"],
},
},
blockExplorers: {
etherscan: { name: "Arbiscan", url: "https://goerli.arbiscan.io/" },
default: { name: "Arbiscan", url: "https://goerli.arbiscan.io/" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 88114,
},
},
testnet: true,
};
const avalanche: Chain = {
id: 43_114,
name: "Avalanche",
network: "avalanche",
nativeCurrency: {
decimals: 18,
name: "Avalanche",
symbol: "AVAX",
},
rpcUrls: {
default: { http: ["https://api.avax.network/ext/bc/C/rpc"] },
public: { http: ["https://api.avax.network/ext/bc/C/rpc"] },
},
blockExplorers: {
etherscan: { name: "SnowTrace", url: "https://snowtrace.io" },
default: { name: "SnowTrace", url: "https://snowtrace.io" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 11907934,
},
},
};
const avalancheFuji: Chain = {
id: 43_113,
name: "Avalanche Fuji",
network: "avalanche-fuji",
nativeCurrency: {
decimals: 18,
name: "Avalanche Fuji",
symbol: "AVAX",
},
rpcUrls: {
default: { http: ["https://api.avax-test.network/ext/bc/C/rpc"] },
public: { http: ["https://api.avax-test.network/ext/bc/C/rpc"] },
},
blockExplorers: {
etherscan: { name: "SnowTrace", url: "https://testnet.snowtrace.io" },
default: { name: "SnowTrace", url: "https://testnet.snowtrace.io" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 7096959,
},
},
testnet: true,
};
const bsc: Chain = {
id: 56,
name: "BNB Smart Chain",
network: "bsc",
nativeCurrency: {
decimals: 18,
name: "BNB",
symbol: "BNB",
},
rpcUrls: {
default: { http: ["https://rpc.ankr.com/bsc"] },
public: { http: ["https://rpc.ankr.com/bsc"] },
},
blockExplorers: {
etherscan: { name: "BscScan", url: "https://bscscan.com" },
default: { name: "BscScan", url: "https://bscscan.com" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 15921452,
},
},
};
const bscTestnet: Chain = {
id: 97,
name: "Binance Smart Chain Testnet",
network: "bsc-testnet",
nativeCurrency: {
decimals: 18,
name: "BNB",
symbol: "tBNB",
},
rpcUrls: {
default: { http: ["https://bsc-testnet.public.blastapi.io"] },
public: { http: ["https://bsc-testnet.public.blastapi.io"] },
},
blockExplorers: {
etherscan: { name: "BscScan", url: "https://testnet.bscscan.com" },
default: { name: "BscScan", url: "https://testnet.bscscan.com" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 17422483,
},
},
testnet: true,
};
const fantom: Chain = {
id: 250,
name: "Fantom",
network: "fantom",
nativeCurrency: {
decimals: 18,
name: "Fantom",
symbol: "FTM",
},
rpcUrls: {
default: { http: ["https://rpc.ankr.com/fantom"] },
public: { http: ["https://rpc.ankr.com/fantom"] },
},
blockExplorers: {
etherscan: { name: "FTMScan", url: "https://ftmscan.com" },
default: { name: "FTMScan", url: "https://ftmscan.com" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 33001987,
},
},
};
const fantomTestnet: Chain = {
id: 4002,
name: "Fantom Testnet",
network: "fantom-testnet",
nativeCurrency: {
decimals: 18,
name: "Fantom",
symbol: "FTM",
},
rpcUrls: {
default: { http: ["https://rpc.testnet.fantom.network"] },
public: { http: ["https://rpc.testnet.fantom.network"] },
},
blockExplorers: {
etherscan: { name: "FTMScan", url: "https://testnet.ftmscan.com" },
default: { name: "FTMScan", url: "https://testnet.ftmscan.com" },
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 8328688,
},
},
};
const goerli: Chain = {
id: 5,
network: "goerli",
name: "Goerli",
nativeCurrency: { name: "Goerli Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
alchemy: {
http: ["https://eth-goerli.g.alchemy.com/v2"],
webSocket: ["wss://eth-goerli.g.alchemy.com/v2"],
},
infura: {
http: ["https://goerli.infura.io/v3"],
webSocket: ["wss://goerli.infura.io/ws/v3"],
},
default: {
http: ["https://rpc.ankr.com/eth_goerli"],
},
public: {
http: ["https://rpc.ankr.com/eth_goerli"],
},
},
blockExplorers: {
etherscan: {
name: "Etherscan",
url: "https://goerli.etherscan.io",
},
default: {
name: "Etherscan",
url: "https://goerli.etherscan.io",
},
},
contracts: {
ensRegistry: {
address: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
},
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 6507670,
},
},
testnet: true,
};
const mainnet: Chain = {
id: 1,
network: "homestead",
name: "Ethereum",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
alchemy: {
http: ["https://eth-mainnet.g.alchemy.com/v2"],
webSocket: ["wss://eth-mainnet.g.alchemy.com/v2"],
},
infura: {
http: ["https://mainnet.infura.io/v3"],
webSocket: ["wss://mainnet.infura.io/ws/v3"],
},
default: {
http: ["https://cloudflare-eth.com"],
},
public: {
http: ["https://cloudflare-eth.com"],
},
},
blockExplorers: {
etherscan: {
name: "Etherscan",
url: "https://etherscan.io",
},
default: {
name: "Etherscan",
url: "https://etherscan.io",
},
},
contracts: {
ensRegistry: {
address: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
},
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 14353601,
},
},
};
const optimism: Chain = {
id: 10,
name: "Optimism",
network: "optimism",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
alchemy: {
http: ["https://opt-mainnet.g.alchemy.com/v2"],
webSocket: ["wss://opt-mainnet.g.alchemy.com/v2"],
},
infura: {
http: ["https://optimism-mainnet.infura.io/v3"],
webSocket: ["wss://optimism-mainnet.infura.io/ws/v3"],
},
default: {
http: ["https://mainnet.optimism.io"],
},
public: {
http: ["https://mainnet.optimism.io"],
},
},
blockExplorers: {
etherscan: {
name: "Etherscan",
url: "https://optimistic.etherscan.io",
},
default: {
name: "Etherscan",
url: "https://optimistic.etherscan.io",
},
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 4286263,
},
},
};
const optimismGoerli: Chain = {
id: 420,
name: "Optimism Goerli",
network: "optimism-goerli",
nativeCurrency: { name: "Goerli Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
alchemy: {
http: ["https://opt-goerli.g.alchemy.com/v2"],
webSocket: ["wss://opt-goerli.g.alchemy.com/v2"],
},
infura: {
http: ["https://optimism-goerli.infura.io/v3"],
webSocket: ["wss://optimism-goerli.infura.io/ws/v3"],
},
default: {
http: ["https://goerli.optimism.io"],
},
public: {
http: ["https://goerli.optimism.io"],
},
},
blockExplorers: {
etherscan: {
name: "Etherscan",
url: "https://goerli-optimism.etherscan.io",
},
default: {
name: "Etherscan",
url: "https://goerli-optimism.etherscan.io",
},
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 49461,
},
},
testnet: true,
};
const polygon: Chain = {
id: 137,
name: "Polygon",
network: "matic",
nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
rpcUrls: {
alchemy: {
http: ["https://polygon-mainnet.g.alchemy.com/v2"],
webSocket: ["wss://polygon-mainnet.g.alchemy.com/v2"],
},
infura: {
http: ["https://polygon-mainnet.infura.io/v3"],
webSocket: ["wss://polygon-mainnet.infura.io/ws/v3"],
},
default: {
http: ["https://polygon-rpc.com"],
},
public: {
http: ["https://polygon-rpc.com"],
},
},
blockExplorers: {
etherscan: {
name: "PolygonScan",
url: "https://polygonscan.com",
},
default: {
name: "PolygonScan",
url: "https://polygonscan.com",
},
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 25770160,
},
},
};
const polygonMumbai: Chain = {
id: 80_001,
name: "Polygon Mumbai",
network: "maticmum",
nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
rpcUrls: {
alchemy: {
http: ["https://polygon-mumbai.g.alchemy.com/v2"],
webSocket: ["wss://polygon-mumbai.g.alchemy.com/v2"],
},
infura: {
http: ["https://polygon-mumbai.infura.io/v3"],
webSocket: ["wss://polygon-mumbai.infura.io/ws/v3"],
},
default: {
http: ["https://matic-mumbai.chainstacklabs.com"],
},
public: {
http: ["https://matic-mumbai.chainstacklabs.com"],
},
},
blockExplorers: {
etherscan: {
name: "PolygonScan",
url: "https://mumbai.polygonscan.com",
},
default: {
name: "PolygonScan",
url: "https://mumbai.polygonscan.com",
},
},
contracts: {
multicall3: {
address: "0xca11bde05977b3631167028862be2a173976ca11",
blockCreated: 25770160,
},
},
testnet: true,
};
/**
* @public
*/
export enum ChainId {
Mainnet = 1,
Goerli = 5,
Polygon = 137,
Mumbai = 80001,
Fantom = 250,
FantomTestnet = 4002,
Avalanche = 43114,
AvalancheFujiTestnet = 43113,
Optimism = 10,
OptimismGoerli = 420,
Arbitrum = 42161,
ArbitrumGoerli = 421613,
BinanceSmartChainMainnet = 56,
BinanceSmartChainTestnet = 97,
}
/**
* @public
*/
export type SUPPORTED_CHAIN_ID =
| ChainId.Mainnet
| ChainId.Goerli
| ChainId.Mumbai
| ChainId.Polygon
| ChainId.Fantom
| ChainId.FantomTestnet
| ChainId.Avalanche
| ChainId.AvalancheFujiTestnet
| ChainId.Optimism
| ChainId.OptimismGoerli
| ChainId.Arbitrum
| ChainId.ArbitrumGoerli
| ChainId.BinanceSmartChainMainnet
| ChainId.BinanceSmartChainTestnet;
/**
* @public
*/
export const SUPPORTED_CHAIN_IDS: SUPPORTED_CHAIN_ID[] = [
ChainId.Mainnet,
ChainId.Goerli,
ChainId.Polygon,
ChainId.Mumbai,
ChainId.Fantom,
ChainId.FantomTestnet,
ChainId.Avalanche,
ChainId.AvalancheFujiTestnet,
ChainId.Optimism,
ChainId.OptimismGoerli,
ChainId.Arbitrum,
ChainId.ArbitrumGoerli,
ChainId.BinanceSmartChainMainnet,
ChainId.BinanceSmartChainTestnet,
];
/**
* @public
*/
export const NATIVE_TOKEN_ADDRESS =
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
export const DEFAULT_API_KEY =
"c6634ad2d97b74baf15ff556016830c251050e6c36b9da508ce3ec80095d3dc1";
function getRpcNameFromChainId(chainId: SUPPORTED_CHAIN_ID): string {
switch (chainId) {
case ChainId.Mainnet:
return "mainnet";
case ChainId.Goerli:
return "goerli";
case ChainId.Polygon:
return "polygon";
case ChainId.Mumbai:
return "mumbai";
case ChainId.Avalanche:
return "avalanche";
case ChainId.AvalancheFujiTestnet:
return "avalanche-fuji";
case ChainId.Fantom:
return "fantom";
case ChainId.FantomTestnet:
return "fantom-testnet";
case ChainId.Arbitrum:
return "arbitrum";
case ChainId.ArbitrumGoerli:
return "arbitrum-goerli";
case ChainId.Optimism:
return "optimism";
case ChainId.OptimismGoerli:
return "optimism-goerli";
case ChainId.BinanceSmartChainMainnet:
return "bsc";
case ChainId.BinanceSmartChainTestnet:
return "bsc-testnet";
default:
throw new Error("Unsupported chain id");
}
}
export function getRpcUrl(chainId: SUPPORTED_CHAIN_ID) {
return `https://${getRpcNameFromChainId(
chainId,
)}.rpc.thirdweb.com/${DEFAULT_API_KEY}`;
}
function enhanceChain<TChain extends Chain>(chain: TChain) {
const twRPC = getRpcUrl(chain.id);
return {
...chain,
rpcUrls: {
...chain.rpcUrls,
default: {
...chain.rpcUrls.default,
http: [twRPC, ...chain.rpcUrls.default.http],
},
public: {
...chain.rpcUrls.public,
http: [twRPC, ...(chain.rpcUrls.public?.http || [])],
},
},
} as TChain;
}
export const supportedChains: Record<SUPPORTED_CHAIN_ID, Chain> = {
[ChainId.Mainnet]: enhanceChain(mainnet),
[ChainId.Goerli]: enhanceChain(goerli),
[ChainId.Polygon]: enhanceChain(polygon),
[ChainId.Mumbai]: enhanceChain(polygonMumbai),
[ChainId.Avalanche]: enhanceChain(avalanche),
[ChainId.AvalancheFujiTestnet]: enhanceChain(avalancheFuji),
[ChainId.Fantom]: enhanceChain(fantom),
[ChainId.FantomTestnet]: enhanceChain(fantomTestnet),
[ChainId.Arbitrum]: enhanceChain(arbitrum),
[ChainId.ArbitrumGoerli]: enhanceChain(arbitrumGoerli),
[ChainId.Optimism]: enhanceChain(optimism),
[ChainId.OptimismGoerli]: enhanceChain(optimismGoerli),
[ChainId.BinanceSmartChainMainnet]: enhanceChain(bsc),
[ChainId.BinanceSmartChainTestnet]: enhanceChain(bscTestnet),
};
export const thirdwebChains: Chain[] = Object.values(supportedChains);
+93
View File
@@ -0,0 +1,93 @@
import { SUPPORTED_CHAIN_ID, supportedChains } from "./evm";
import type { Ecosystem, GenericAuthWallet } from "@thirdweb-dev/wallets";
import { ethers } from "ethers";
const EIP1271_ABI = [
"function isValidSignature(bytes32 _message, bytes _signature) public view returns (bytes4)",
];
const EIP1271_MAGICVALUE = "0x1626ba7e";
export const checkContractWalletSignature = async (
message: string,
signature: string,
address: string,
chainId: number,
): Promise<boolean> => {
const rpcUrl =
supportedChains[chainId as SUPPORTED_CHAIN_ID]?.rpcUrls.default.http[0];
if (!rpcUrl) {
return false;
}
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const walletContract = new ethers.Contract(address, EIP1271_ABI, provider);
const hashMessage = ethers.utils.hashMessage(message);
try {
const res = await walletContract.isValidSignature(hashMessage, signature);
return res === EIP1271_MAGICVALUE;
} catch {
return false;
}
};
export class SignerWallet implements GenericAuthWallet {
type: Ecosystem = "evm";
#signer: ethers.Signer;
constructor(signer: ethers.Signer) {
this.#signer = signer;
}
public async getAddress(): Promise<string> {
return this.#signer.getAddress();
}
public async getChainId(): Promise<number> {
return this.#signer.getChainId();
}
public async signMessage(message: string): Promise<string> {
return await this.#signer.signMessage(message);
}
public async verifySignature(
message: string,
signature: string,
address: string,
chainId?: number,
): Promise<boolean> {
const messageHash = ethers.utils.hashMessage(message);
const messageHashBytes = ethers.utils.arrayify(messageHash);
const recoveredAddress = ethers.utils.recoverAddress(
messageHashBytes,
signature,
);
if (recoveredAddress === address) {
return true;
}
// Check if the address is a smart contract wallet
if (chainId !== undefined) {
try {
const isValid = await checkContractWalletSignature(
message,
signature,
address,
chainId || 1,
);
return isValid;
} catch {
// no-op
}
}
return false;
}
}
export class PrivateKeyWallet extends SignerWallet {
constructor(privateKey: string) {
super(new ethers.Wallet(privateKey));
}
}
-68
View File
@@ -1,68 +0,0 @@
import loginHandler from "./routes/login";
import logoutHandler from "./routes/logout";
import userHandler from "./routes/user";
import {
ThirdwebAuthConfig,
ThirdwebAuthRoute,
ThirdwebAuthUser,
} from "./types";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import cookieParser from "cookie-parser";
import { Express, NextFunction, Request, Response } from "express";
export * from "./types";
export function getUser(req: Request): ThirdwebAuthUser | null {
return req.user;
}
export function ThirdwebAuth(app: Express, cfg: ThirdwebAuthConfig) {
const ctx = {
...cfg,
sdk: ThirdwebSDK.fromPrivateKey(cfg.privateKey, "mainnet"),
};
const authUrl = cfg.authUrl?.replace(/\/$/, "") || "/auth";
app.use(cookieParser());
app.use(async (req: Request, _: Response, next: NextFunction) => {
const { sdk, domain } = ctx;
let user = null;
const token = req.cookies.thirdweb_auth_token;
if (token) {
try {
const address = await sdk.auth.authenticate(domain, token);
if (ctx.callbacks?.user) {
user = await ctx.callbacks.user(address);
}
user = { ...user, address };
} catch {
// No-op
}
}
req.user = user as ThirdwebAuthUser | null;
next();
});
app.get(`${authUrl}/:route`, (req: Request, res: Response) => {
const action = req.params.route as ThirdwebAuthRoute;
switch (action) {
case "login":
return loginHandler(req, res, ctx);
case "user":
return userHandler(req, res);
case "logout":
return logoutHandler(req, res);
default:
return res.status(400).json({
message: "Invalid route for authentication.",
});
}
});
}
@@ -1,56 +0,0 @@
import { ThirdwebAuthContext } from "../types";
import { LoginPayload } from "@thirdweb-dev/sdk";
import { serialize } from "cookie";
import { Request, Response } from "express";
function redirectWithError(req: Request, res: Response, error: string) {
const encodedError = encodeURIComponent(error);
const url = new URL(req.headers.referer as string);
url.searchParams.set("error", encodedError);
return res.redirect(url.toString());
}
export default async function handler(
req: Request,
res: Response,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "GET") {
return redirectWithError(req, res, "INVALID_METHOD");
}
const { sdk, domain } = ctx;
// Get signed login payload from the frontend
const payload = JSON.parse(atob(req.query.payload as string)) as LoginPayload;
if (!payload) {
redirectWithError(req, res, "MISSING_LOGIN_PAYLOAD");
}
let token;
try {
// Generate an access token with the SDK using the signed payload
token = await sdk.auth.generateAuthToken(domain, payload);
} catch {
return redirectWithError(req, res, "INVALID_LOGIN_PAYLOAD");
}
// Securely set httpOnly cookie on request to prevent XSS on frontend
// And set path to / to enable thirdweb_auth_token usage on all endpoints
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", token, {
path: "/",
httpOnly: true,
secure: true,
sameSite: "none",
}),
);
if (ctx.callbacks?.login) {
const address = sdk.auth.verify(domain, payload);
await ctx.callbacks.login(address);
}
return res.status(301).redirect(req.query.redirect as string);
}
@@ -1,21 +0,0 @@
import { serialize } from "cookie";
import { Request, Response } from "express";
export default async function handler(req: Request, res: Response) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
// Set the access token to 'none' and expire in 5 seconds
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", "", {
path: "/",
expires: new Date(Date.now() + 5 * 1000),
}),
);
return res.status(301).redirect(req.headers.referer as string);
}
@@ -1,11 +0,0 @@
import { Request, Response } from "express";
export default async function handler(req: Request, res: Response) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
return res.status(200).json(req.user);
}
-12
View File
@@ -1,12 +0,0 @@
import { ThirdwebAuthUser } from "..";
export {};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Express {
export interface Request {
user: ThirdwebAuthUser | null;
}
}
}
@@ -1,40 +0,0 @@
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { Request } from "express";
export type ThirdwebAuthRoute = "login" | "user" | "logout";
export type ThirdwebAuthConfig = {
privateKey: string;
domain: string;
authUrl?: string;
callbacks?: {
login?: (address: string) => Promise<void> | void;
user?: (
address: string,
) =>
| Promise<Omit<ThirdwebAuthUser, "address">>
| Omit<ThirdwebAuthUser, "address">;
};
};
export type ThirdwebAuthContext = {
sdk: ThirdwebSDK;
domain: string;
callbacks?: {
login?: (address: string) => Promise<void> | void;
user?: (
address: string,
) =>
| Promise<Omit<ThirdwebAuthUser, "address">>
| Omit<ThirdwebAuthUser, "address">;
};
};
export type ThirdwebAuthUser = {
address: string;
[key: string]: any;
};
export type RequestWithUser = Request & {
user: ThirdwebAuthUser | null;
};
+60
View File
@@ -0,0 +1,60 @@
import { Json } from "../../core/schema";
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
import { Request } from "express";
function getToken(req: Request): string | undefined {
if (req.headers["authorization"]) {
const authorizationHeader = req.headers["authorization"].split(" ");
if (authorizationHeader?.length === 2) {
return authorizationHeader[1];
}
return undefined;
}
const cookie: string | undefined = !req.cookies
? undefined
: typeof req.cookies.get === "function"
? (req.cookies as any).get("thirdweb_auth_token")
: (req.cookies as any).thirdweb_auth_token;
return cookie;
}
export async function getUser<
TData extends Json = Json,
TSession extends Json = Json,
>(
req: Request,
ctx: ThirdwebAuthContext<TData, TSession>,
): Promise<ThirdwebAuthUser<TData, TSession> | null> {
const token = getToken(req);
if (!token) {
return null;
}
let authenticatedUser: ThirdwebAuthUser<TData, TSession>;
try {
authenticatedUser = await ctx.auth.authenticate<TSession>(token, {
validateTokenId: async (tokenId: string) => {
if (ctx.authOptions?.validateTokenId) {
await ctx.authOptions?.validateTokenId(tokenId);
}
},
});
} catch (err) {
return null;
}
if (!ctx.callbacks?.onUser) {
return authenticatedUser;
}
const data = await ctx.callbacks.onUser(authenticatedUser);
if (!data) {
return authenticatedUser;
}
return { ...authenticatedUser, data: data };
}
+63 -1
View File
@@ -1 +1,63 @@
export * from "./evm";
import { Json, ThirdwebAuth as ThirdwebAuthSDK } from "../core";
import { getUser } from "./helpers/user";
import loginHandler from "./routes/login";
import logoutHandler from "./routes/logout";
import userHandler from "./routes/user";
import { ThirdwebAuthConfig, ThirdwebAuthContext } from "./types";
import cookieParser from "cookie-parser";
import express, { Request, Response } from "express";
export * from "./types";
const asyncHandler =
(fn: CallableFunction) =>
(...args: any[]) => {
const fnReturn = fn(...args);
const next = args[args.length - 1];
return Promise.resolve(fnReturn).catch(next);
};
export function ThirdwebAuth<
TData extends Json = Json,
TSession extends Json = Json,
>(cfg: ThirdwebAuthConfig<TData, TSession>) {
const ctx = {
...cfg,
auth: new ThirdwebAuthSDK(cfg.wallet, cfg.domain),
};
const router = express.Router();
const cookieMiddleware = cookieParser();
router.use(express.json());
router.use(cookieMiddleware);
router.post(
"/login",
asyncHandler((req: Request, res: Response) =>
loginHandler(req, res, ctx as ThirdwebAuthContext),
),
);
router.get(
"/user",
asyncHandler((req: Request, res: Response) =>
userHandler(req, res, ctx as ThirdwebAuthContext),
),
);
router.post(
"/logout",
asyncHandler((req: Request, res: Response) =>
logoutHandler(req, res, ctx as ThirdwebAuthContext),
),
);
return {
authRouter: router,
authMiddleware: cookieMiddleware,
getUser: (req: Request) => {
return getUser<TData, TSession>(req, ctx);
},
};
}
+82
View File
@@ -0,0 +1,82 @@
import { GenerateOptions } from "../../core";
import { LoginPayloadBodySchema, ThirdwebAuthContext } from "../types";
import { serialize } from "cookie";
import { Request, Response } from "express";
export default async function handler(
req: Request,
res: Response,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const parsedPayload = LoginPayloadBodySchema.safeParse(req.body);
// Get signed login payload from the frontend
if (!parsedPayload.success) {
return res.status(400).json({ error: "Invalid login payload" });
}
const payload = parsedPayload.data.payload;
const validateNonce = async (nonce: string) => {
if (ctx.authOptions?.validateNonce) {
await ctx.authOptions?.validateNonce(nonce);
}
};
const getSession = async (address: string) => {
if (ctx.callbacks?.onLogin) {
return ctx.callbacks.onLogin(address, req);
}
};
const expirationTime = ctx.authOptions?.tokenDurationInSeconds
? new Date(Date.now() + 1000 * ctx.authOptions.tokenDurationInSeconds)
: undefined;
const generateOptions: GenerateOptions = {
verifyOptions: {
statement: ctx.authOptions?.statement,
uri: ctx.authOptions?.uri,
version: ctx.authOptions?.version,
chainId: ctx.authOptions?.chainId,
validateNonce,
resources: ctx.authOptions?.resources,
},
expirationTime,
session: getSession,
};
let token: string;
try {
// Generate an access token with the SDK using the signed payload
token = await ctx.auth.generate(payload, generateOptions);
} catch (err: any) {
if (err.message) {
return res.status(403).json({ error: err.message });
} else if (typeof err === "string") {
return res.status(403).json({ error: err });
} else {
return res.status(403).json({ error: "Invalid login payload" });
}
}
// Securely set httpOnly cookie on request to prevent XSS on frontend
// And set path to / to enable thirdweb_auth_token usage on all endpoints
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", token, {
domain: ctx.cookieOptions?.domain,
path: ctx.cookieOptions?.path || "/",
sameSite: ctx.cookieOptions?.sameSite || "none",
httpOnly: true,
secure: true,
}),
);
// Send token in body and as cookie for frontend and backend use cases
return res.status(200).json({ token });
}
@@ -0,0 +1,35 @@
import { getUser } from "../helpers/user";
import { ThirdwebAuthContext } from "../types";
import { serialize } from "cookie";
import { Request, Response } from "express";
export default async function handler(
req: Request,
res: Response,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "POST") {
return res.status(405).json({
error: "Invalid method. Only POST supported.",
});
}
if (ctx.callbacks?.onLogout) {
const user = await getUser(req, ctx);
if (user) {
await ctx.callbacks.onLogout(user, req);
}
}
// Set the access token to 'none' and expire in 5 seconds
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", "", {
domain: ctx.cookieOptions?.domain,
path: ctx.cookieOptions?.path || "/",
expires: new Date(Date.now() + 5 * 1000),
}),
);
return res.status(200).json({ message: "Succesfully logged out" });
}
+18
View File
@@ -0,0 +1,18 @@
import { getUser } from "../helpers/user";
import { ThirdwebAuthContext } from "../types";
import { Request, Response } from "express";
export default async function handler(
req: Request,
res: Response,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
const user = await getUser(req, ctx);
return res.status(200).json(user);
}
+63
View File
@@ -0,0 +1,63 @@
import { ThirdwebAuth } from "../../core";
import { Json, LoginPayloadOutputSchema, User } from "../../core/schema";
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { Request } from "express";
import { z } from "zod";
export const LoginPayloadBodySchema = z.object({
payload: LoginPayloadOutputSchema,
});
export type ThirdwebAuthRoute = "login" | "user" | "logout";
export type ThirdwebAuthUser<
TData extends Json = Json,
TSession extends Json = Json,
> = User<TSession> & {
data?: TData;
};
export type ThirdwebAuthConfig<
TData extends Json = Json,
TSession extends Json = Json,
> = {
domain: string;
wallet: GenericAuthWallet;
authOptions?: {
statement?: string;
uri?: string;
version?: string;
chainId?: string;
resources?: string[];
validateNonce?:
| ((nonce: string) => void)
| ((nonce: string) => Promise<void>);
validateTokenId?:
| ((tokenId: string) => void)
| ((tokenId: string) => Promise<void>);
tokenDurationInSeconds?: number;
};
cookieOptions?: {
domain?: string;
path?: string;
sameSite?: "lax" | "strict" | "none";
};
callbacks?: {
onLogin?:
| ((address: string, req?: Request) => void | TSession)
| ((address: string, req?: Request) => Promise<void | TSession>);
onUser?:
| ((user: User<TSession>, req?: Request) => void | TData)
| ((user: User<TSession>, req?: Request) => Promise<void | TData>);
onLogout?:
| ((user: User, req?: Request) => void)
| ((user: User, req?: Request) => Promise<void>);
};
};
export type ThirdwebAuthContext<
TData extends Json = Json,
TSession extends Json = Json,
> = Omit<Omit<ThirdwebAuthConfig<TData, TSession>, "wallet">, "domain"> & {
auth: ThirdwebAuth;
};
+1
View File
@@ -0,0 +1 @@
export * from "./core";
-145
View File
@@ -1,145 +0,0 @@
import { ThirdwebNextAuthConfig } from "./types";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { serialize } from "cookie";
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next";
import NextAuth, {
NextAuthOptions,
Session,
unstable_getServerSession,
} from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export function ThirdwebNextAuth(cfg: ThirdwebNextAuthConfig) {
const sdk = ThirdwebSDK.fromPrivateKey(cfg.privateKey, "mainnet");
function ThirdwebProvider(res: GetServerSidePropsContext["res"]) {
return CredentialsProvider({
name: "ThirdwebAuth",
credentials: {
payload: {
label: "Payload",
type: "text",
placeholder: "",
},
},
async authorize({ payload }: any) {
try {
const parsed = JSON.parse(payload);
const token = await sdk.auth.generateAuthToken(cfg.domain, parsed);
const address = await sdk.auth.authenticate(cfg.domain, token);
// Securely set httpOnly cookie on request to prevent XSS on frontend
// And set path to / to enable thirdweb_auth_token usage on all endpoints
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", token, {
path: "/",
httpOnly: true,
secure: true,
sameSite: "none",
}),
);
return { id: address, address };
} catch (err) {
return null;
}
},
});
}
function nextOptions(
req: GetServerSidePropsContext["req"],
res: GetServerSidePropsContext["res"],
): NextAuthOptions {
async function session({
session: _session,
}: {
session: Session;
}): Promise<Session> {
const token = req.cookies.thirdweb_auth_token || "";
try {
const address = await sdk.auth.authenticate(cfg.domain, token);
_session.user = { ..._session.user, address } as Session["user"];
return _session;
} catch {
return _session;
}
}
function signOut() {
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", "", {
path: "/",
expires: new Date(Date.now() + 5 * 1000),
}),
);
}
const providers: NextAuthOptions["providers"] = [
...cfg.nextOptions.providers,
ThirdwebProvider(res),
];
const configSession = cfg.nextOptions.callbacks?.session;
const callbacks: NextAuthOptions["callbacks"] = {
...cfg.nextOptions.callbacks,
session: configSession
? async (params) => {
params.session = await session(params);
return configSession(params);
}
: session,
};
const configSignOut = cfg.nextOptions.events?.signOut;
const events: NextAuthOptions["events"] = {
...cfg.nextOptions.events,
signOut: configSignOut
? async (params) => {
signOut();
return configSignOut(params);
}
: signOut,
};
return {
...cfg.nextOptions,
providers,
callbacks,
events,
};
}
async function getUser(
...args:
| [NextApiRequest, NextApiResponse]
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
) {
return unstable_getServerSession(
args[0],
args[1],
nextOptions(args[0], args[1]),
);
}
function NextAuthHandler(...args: [] | [NextApiRequest, NextApiResponse]) {
if (args.length === 0) {
return (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextOptions(req, res));
};
}
return NextAuth(args[0], args[1], nextOptions(args[0], args[1]));
}
return {
NextAuthHandler,
getUser,
};
}
+145 -1
View File
@@ -1 +1,145 @@
export * from "./evm";
import { ThirdwebAuth } from "../core";
import { ThirdwebNextAuthConfig } from "./types";
import { serialize } from "cookie";
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next";
import NextAuth, {
NextAuthOptions,
Session,
unstable_getServerSession,
} from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export function ThirdwebNextAuth(cfg: ThirdwebNextAuthConfig) {
const auth = new ThirdwebAuth(cfg.wallet, cfg.domain);
function ThirdwebProvider(res: GetServerSidePropsContext["res"]) {
return CredentialsProvider({
name: "ThirdwebAuth",
credentials: {
payload: {
label: "Payload",
type: "text",
placeholder: "",
},
},
async authorize({ payload }: any) {
try {
const parsed = JSON.parse(payload);
const token = await auth.generate(parsed);
const user = await auth.authenticate(token);
// Securely set httpOnly cookie on request to prevent XSS on frontend
// And set path to / to enable thirdweb_auth_token usage on all endpoints
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", token, {
path: "/",
httpOnly: true,
secure: true,
sameSite: "none",
}),
);
return { id: user.address, address: user.address };
} catch (err) {
return null;
}
},
});
}
function nextOptions(
req: GetServerSidePropsContext["req"],
res: GetServerSidePropsContext["res"],
): NextAuthOptions {
async function session({
session: _session,
}: {
session: Session;
}): Promise<Session> {
const token = req.cookies.thirdweb_auth_token || "";
try {
const address = await auth.authenticate(token);
_session.user = { ..._session.user, address } as Session["user"];
return _session;
} catch {
return _session;
}
}
function signOut() {
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", "", {
path: "/",
expires: new Date(Date.now() + 5 * 1000),
}),
);
}
const providers: NextAuthOptions["providers"] = [
...cfg.nextOptions.providers,
ThirdwebProvider(res),
];
const configSession = cfg.nextOptions.callbacks?.session;
const callbacks: NextAuthOptions["callbacks"] = {
...cfg.nextOptions.callbacks,
session: configSession
? async (params) => {
params.session = await session(params);
return configSession(params);
}
: session,
};
const configSignOut = cfg.nextOptions.events?.signOut;
const events: NextAuthOptions["events"] = {
...cfg.nextOptions.events,
signOut: configSignOut
? async (params) => {
signOut();
return configSignOut(params);
}
: signOut,
};
return {
...cfg.nextOptions,
providers,
callbacks,
events,
};
}
async function getUser(
...args:
| [NextApiRequest, NextApiResponse]
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
) {
return unstable_getServerSession(
args[0],
args[1],
nextOptions(args[0], args[1]),
);
}
function NextAuthHandler(...args: [] | [NextApiRequest, NextApiResponse]) {
if (args.length === 0) {
return (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextOptions(req, res));
};
}
return NextAuth(args[0], args[1], nextOptions(args[0], args[1]));
}
return {
NextAuthHandler,
getUser,
};
}
@@ -1,7 +1,8 @@
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { NextAuthOptions } from "next-auth";
export type ThirdwebNextAuthConfig = {
privateKey: string;
domain: string;
wallet: GenericAuthWallet;
nextOptions: NextAuthOptions;
};
-89
View File
@@ -1,89 +0,0 @@
import loginHandler from "./routes/login";
import logoutHandler from "./routes/logout";
import userHandler from "./routes/user";
import {
ThirdwebAuthConfig,
ThirdwebAuthContext,
ThirdwebAuthRoute,
ThirdwebAuthUser,
} from "./types";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { NextRequest } from "next/server";
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next/types";
export * from "./types";
async function ThirdwebAuthRouter(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
// Catch-all route must be named with [...thirdweb]
const { thirdweb } = req.query;
const action = thirdweb?.[0] as ThirdwebAuthRoute;
switch (action) {
case "login":
return await loginHandler(req, res, ctx);
case "user":
return await userHandler(req, res, ctx);
case "logout":
return await logoutHandler(req, res);
default:
return res.status(400).json({
message: "Invalid route for authentication.",
});
}
}
export function ThirdwebAuth(cfg: ThirdwebAuthConfig) {
const ctx = {
...cfg,
sdk: ThirdwebSDK.fromPrivateKey(cfg.privateKey, "mainnet"),
};
function ThirdwebAuthHandler(
...args: [] | [NextApiRequest, NextApiResponse]
) {
if (args.length === 0) {
return async (req: NextApiRequest, res: NextApiResponse) =>
await ThirdwebAuthRouter(req, res, ctx);
}
return ThirdwebAuthRouter(args[0], args[1], ctx);
}
async function getUser(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
) {
const { sdk, domain } = ctx;
let user: ThirdwebAuthUser | null = null;
const token =
typeof req.cookies.get === "function"
? (req.cookies as any).get("thirdweb_auth_token")
: (req.cookies as any).thirdweb_auth_token;
if (token) {
try {
const address = await sdk.auth.authenticate(domain, token);
let data = {};
if (ctx.callbacks?.user) {
data = await ctx.callbacks.user(address);
}
user = { ...data, address };
} catch {
// No-op
}
}
return user;
}
return { ThirdwebAuthHandler, getUser };
}
@@ -1,60 +0,0 @@
import { ThirdwebAuthContext } from "../types";
import { LoginPayload } from "@thirdweb-dev/sdk";
import { serialize } from "cookie";
import { NextApiRequest, NextApiResponse } from "next";
function redirectWithError(
req: NextApiRequest,
res: NextApiResponse,
error: string,
) {
const encodedError = encodeURIComponent(error);
const url = new URL(req.headers.referer as string);
url.searchParams.set("error", encodedError);
return res.redirect(url.toString());
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "GET") {
return redirectWithError(req, res, "INVALID_METHOD");
}
const { sdk, domain } = ctx;
// Get signed login payload from the frontend
const payload = JSON.parse(atob(req.query.payload as string)) as LoginPayload;
if (!payload) {
redirectWithError(req, res, "MISSING_LOGIN_PAYLOAD");
}
let token;
try {
// Generate an access token with the SDK using the signed payload
token = await sdk.auth.generateAuthToken(domain, payload);
} catch {
return redirectWithError(req, res, "INVALID_LOGIN_PAYLOAD");
}
// Securely set httpOnly cookie on request to prevent XSS on frontend
// And set path to / to enable thirdweb_auth_token usage on all endpoints
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", token, {
path: "/",
httpOnly: true,
secure: true,
sameSite: "none",
}),
);
if (ctx.callbacks?.login) {
const address = sdk.auth.verify(domain, payload);
await ctx.callbacks.login(address);
}
return res.status(301).redirect(req.query.redirect as string);
}
@@ -1,24 +0,0 @@
import { serialize } from "cookie";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
// Set the access token to 'none' and expire in 5 seconds
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", "", {
path: "/",
expires: new Date(Date.now() + 5 * 1000),
}),
);
return res.status(301).redirect(req.headers.referer as string);
}
-34
View File
@@ -1,34 +0,0 @@
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
const { sdk, domain } = ctx;
let user = null;
const token = req.cookies.thirdweb_auth_token;
if (token) {
try {
const address = await sdk.auth.authenticate(domain, token);
if (ctx.callbacks?.user) {
user = await ctx.callbacks.user(address);
}
user = { ...user, address };
} catch {
// No-op
}
}
return res.status(200).json(user as ThirdwebAuthUser | null);
}
-34
View File
@@ -1,34 +0,0 @@
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
export type ThirdwebAuthRoute = "login" | "logout" | "user";
export type ThirdwebAuthConfig = {
privateKey: string;
domain: string;
callbacks?: {
login?: (address: string) => Promise<void> | void;
user?: (
address: string,
) =>
| Promise<Omit<ThirdwebAuthUser, "address">>
| Omit<ThirdwebAuthUser, "address">;
};
};
export type ThirdwebAuthContext = {
sdk: ThirdwebSDK;
domain: string;
callbacks?: {
login?: (address: string) => Promise<void> | void;
user?: (
address: string,
) =>
| Promise<Omit<ThirdwebAuthUser, "address">>
| Omit<ThirdwebAuthUser, "address">;
};
};
export type ThirdwebAuthUser = {
address: string;
[key: string]: any;
};
+65
View File
@@ -0,0 +1,65 @@
import { Json } from "../../core/schema";
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
import { GetServerSidePropsContext, NextApiRequest } from "next";
import { NextRequest } from "next/server";
function getToken(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
): string | undefined {
if (!!(req as NextApiRequest).headers["authorization"]) {
const authorizationHeader = (req as NextApiRequest).headers[
"authorization"
]?.split(" ");
if (authorizationHeader?.length === 2) {
return authorizationHeader[1];
}
return undefined;
}
const cookie: string | undefined = !req.cookies
? undefined
: typeof req.cookies.get === "function"
? (req.cookies as any).get("thirdweb_auth_token")
: (req.cookies as any).thirdweb_auth_token;
return cookie;
}
export async function getUser<
TData extends Json = Json,
TSession extends Json = Json,
>(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
ctx: ThirdwebAuthContext<TData, TSession>,
): Promise<ThirdwebAuthUser<TData, TSession> | null> {
const token = getToken(req);
if (!token) {
return null;
}
let authenticatedUser: ThirdwebAuthUser<TData, TSession>;
try {
authenticatedUser = await ctx.auth.authenticate<TSession>(token, {
validateTokenId: async (tokenId: string) => {
if (ctx.authOptions?.validateTokenId) {
await ctx.authOptions?.validateTokenId(tokenId);
}
},
});
} catch (err) {
return null;
}
if (!ctx.callbacks?.onUser) {
return authenticatedUser;
}
const data = await ctx.callbacks.onUser(authenticatedUser);
if (!data) {
return authenticatedUser;
}
return { ...authenticatedUser, data: data };
}
+71 -1
View File
@@ -1 +1,71 @@
export * from "./evm";
import { Json, ThirdwebAuth as ThirdwebAuthSDK } from "../core";
import { getUser } from "./helpers/user";
import loginHandler from "./routes/login";
import logoutHandler from "./routes/logout";
import userHandler from "./routes/user";
import {
ThirdwebAuthConfig,
ThirdwebAuthContext,
ThirdwebAuthRoute,
} from "./types";
import { NextRequest } from "next/server";
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next/types";
export * from "./types";
async function ThirdwebAuthRouter(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
// Catch-all route must be named with [...thirdweb]
const { thirdweb } = req.query;
const action = thirdweb?.[0] as ThirdwebAuthRoute;
switch (action) {
case "login":
return await loginHandler(req, res, ctx);
case "user":
return await userHandler(req, res, ctx);
case "logout":
return await logoutHandler(req, res, ctx);
default:
return res.status(400).json({
message: "Invalid route for authentication.",
});
}
}
export function ThirdwebAuth<
TData extends Json = Json,
TSession extends Json = Json,
>(cfg: ThirdwebAuthConfig<TData, TSession>) {
const ctx = {
...cfg,
auth: new ThirdwebAuthSDK(cfg.wallet, cfg.domain),
};
function ThirdwebAuthHandler(
...args: [] | [NextApiRequest, NextApiResponse]
) {
if (args.length === 0) {
return async (req: NextApiRequest, res: NextApiResponse) =>
await ThirdwebAuthRouter(req, res, ctx as ThirdwebAuthContext);
}
return ThirdwebAuthRouter(args[0], args[1], ctx as ThirdwebAuthContext);
}
return {
ThirdwebAuthHandler,
getUser: (
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
) => {
return getUser<TData, TSession>(req, ctx);
},
};
}
+82
View File
@@ -0,0 +1,82 @@
import { GenerateOptions } from "../../core";
import { LoginPayloadBodySchema, ThirdwebAuthContext } from "../types";
import { serialize } from "cookie";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const parsedPayload = LoginPayloadBodySchema.safeParse(req.body);
// Get signed login payload from the frontend
if (!parsedPayload.success) {
return res.status(400).json({ error: "Invalid login payload" });
}
const payload = parsedPayload.data.payload;
const validateNonce = async (nonce: string) => {
if (ctx.authOptions?.validateNonce) {
await ctx.authOptions?.validateNonce(nonce);
}
};
const getSession = async (address: string) => {
if (ctx.callbacks?.onLogin) {
return ctx.callbacks.onLogin(address, req);
}
};
const expirationTime = ctx.authOptions?.tokenDurationInSeconds
? new Date(Date.now() + 1000 * ctx.authOptions.tokenDurationInSeconds)
: undefined;
const generateOptions: GenerateOptions = {
verifyOptions: {
statement: ctx.authOptions?.statement,
uri: ctx.authOptions?.uri,
version: ctx.authOptions?.version,
chainId: ctx.authOptions?.chainId,
validateNonce,
resources: ctx.authOptions?.resources,
},
expirationTime,
session: getSession,
};
let token: string;
try {
// Generate an access token with the SDK using the signed payload
token = await ctx.auth.generate(payload, generateOptions);
} catch (err: any) {
if (err.message) {
return res.status(403).json({ error: err.message });
} else if (typeof err === "string") {
return res.status(403).json({ error: err });
} else {
return res.status(403).json({ error: "Invalid login payload" });
}
}
// Securely set httpOnly cookie on request to prevent XSS on frontend
// And set path to / to enable thirdweb_auth_token usage on all endpoints
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", token, {
domain: ctx.cookieOptions?.domain,
path: ctx.cookieOptions?.path || "/",
sameSite: ctx.cookieOptions?.sameSite || "none",
httpOnly: true,
secure: true,
}),
);
// Send token in body and as cookie for frontend and backend use cases
return res.status(200).json({ token });
}
+35
View File
@@ -0,0 +1,35 @@
import { getUser } from "../helpers/user";
import { ThirdwebAuthContext } from "../types";
import { serialize } from "cookie";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "POST") {
return res.status(405).json({
error: "Invalid method. Only POST supported.",
});
}
if (ctx.callbacks?.onLogout) {
const user = await getUser(req, ctx);
if (user) {
await ctx.callbacks.onLogout(user, req);
}
}
// Set the access token to 'none' and expire in 5 seconds
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", "", {
domain: ctx.cookieOptions?.domain,
path: ctx.cookieOptions?.path || "/",
expires: new Date(Date.now() + 5 * 1000),
}),
);
return res.status(200).json({ message: "Succesfully logged out" });
}
+18
View File
@@ -0,0 +1,18 @@
import { getUser } from "../helpers/user";
import { ThirdwebAuthContext } from "../types";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
const user = await getUser(req, ctx);
return res.status(200).json(user);
}
-89
View File
@@ -1,89 +0,0 @@
import loginHandler from "./routes/login";
import logoutHandler from "./routes/logout";
import userHandler from "./routes/user";
import {
ThirdwebAuthConfig,
ThirdwebAuthContext,
ThirdwebAuthRoute,
ThirdwebAuthUser,
} from "./types";
import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
import { NextRequest } from "next/server";
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from "next/types";
export * from "./types";
async function ThirdwebAuthRouter(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
// Catch-all route must be named with [...thirdweb]
const { thirdweb } = req.query;
const action = thirdweb?.[0] as ThirdwebAuthRoute;
switch (action) {
case "login":
return await loginHandler(req, res, ctx);
case "user":
return await userHandler(req, res, ctx);
case "logout":
return await logoutHandler(req, res);
default:
return res.status(400).json({
message: "Invalid route for authentication.",
});
}
}
export function ThirdwebAuth(cfg: ThirdwebAuthConfig) {
const ctx = {
...cfg,
sdk: ThirdwebSDK.fromPrivateKey("mainnet-beta", cfg.privateKey),
};
function ThirdwebAuthHandler(
...args: [] | [NextApiRequest, NextApiResponse]
) {
if (args.length === 0) {
return async (req: NextApiRequest, res: NextApiResponse) =>
await ThirdwebAuthRouter(req, res, ctx);
}
return ThirdwebAuthRouter(args[0], args[1], ctx);
}
async function getUser(
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
) {
const { sdk, domain } = ctx;
let user: ThirdwebAuthUser | null = null;
const token =
typeof req.cookies.get === "function"
? (req.cookies as any).get("thirdweb_auth_token")
: (req.cookies as any).thirdweb_auth_token;
if (token) {
try {
const address = await sdk.auth.authenticate(domain, token);
let data = {};
if (ctx.callbacks?.user) {
data = await ctx.callbacks.user(address);
}
user = { ...data, address };
} catch {
// No-op
}
}
return user;
}
return { ThirdwebAuthHandler, getUser };
}
@@ -1,60 +0,0 @@
import { ThirdwebAuthContext } from "../types";
import { LoginPayload } from "@thirdweb-dev/sdk";
import { serialize } from "cookie";
import { NextApiRequest, NextApiResponse } from "next";
function redirectWithError(
req: NextApiRequest,
res: NextApiResponse,
error: string,
) {
const encodedError = encodeURIComponent(error);
const url = new URL(req.headers.referer as string);
url.searchParams.set("error", encodedError);
return res.redirect(url.toString());
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "GET") {
return redirectWithError(req, res, "INVALID_METHOD");
}
const { sdk, domain } = ctx;
// Get signed login payload from the frontend
const payload = JSON.parse(atob(req.query.payload as string)) as LoginPayload;
if (!payload) {
redirectWithError(req, res, "MISSING_LOGIN_PAYLOAD");
}
let token;
try {
// Generate an access token with the SDK using the signed payload
token = await sdk.auth.generateAuthToken(domain, payload);
} catch {
return redirectWithError(req, res, "INVALID_LOGIN_PAYLOAD");
}
// Securely set httpOnly cookie on request to prevent XSS on frontend
// And set path to / to enable thirdweb_auth_token usage on all endpoints
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", token, {
path: "/",
httpOnly: true,
secure: true,
sameSite: "none",
}),
);
if (ctx.callbacks?.login) {
const address = sdk.auth.verify(domain, payload);
await ctx.callbacks.login(address);
}
return res.status(301).redirect(req.query.redirect as string);
}
@@ -1,24 +0,0 @@
import { serialize } from "cookie";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
// Set the access token to 'none' and expire in 5 seconds
res.setHeader(
"Set-Cookie",
serialize("thirdweb_auth_token", "", {
path: "/",
expires: new Date(Date.now() + 5 * 1000),
}),
);
return res.status(301).redirect(req.headers.referer as string);
}
@@ -1,34 +0,0 @@
import { ThirdwebAuthContext, ThirdwebAuthUser } from "../types";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
ctx: ThirdwebAuthContext,
) {
if (req.method !== "GET") {
return res.status(400).json({
error: "Invalid method. Only GET supported.",
});
}
const { sdk, domain } = ctx;
let user = null;
const token = req.cookies.thirdweb_auth_token;
if (token) {
try {
const address = await sdk.auth.authenticate(domain, token);
if (ctx.callbacks?.user) {
user = await ctx.callbacks.user(address);
}
user = { ...user, address };
} catch {
// No-op
}
}
return res.status(200).json(user as ThirdwebAuthUser | null);
}
@@ -1,34 +0,0 @@
import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
export type ThirdwebAuthRoute = "login" | "logout" | "user";
export type ThirdwebAuthConfig = {
privateKey: string;
domain: string;
callbacks?: {
login?: (address: string) => Promise<void> | void;
user?: (
address: string,
) =>
| Promise<Omit<ThirdwebAuthUser, "address">>
| Omit<ThirdwebAuthUser, "address">;
};
};
export type ThirdwebAuthContext = {
sdk: ThirdwebSDK;
domain: string;
callbacks?: {
login?: (address: string) => Promise<void> | void;
user?: (
address: string,
) =>
| Promise<Omit<ThirdwebAuthUser, "address">>
| Omit<ThirdwebAuthUser, "address">;
};
};
export type ThirdwebAuthUser = {
address: string;
[key: string]: any;
};
+75
View File
@@ -0,0 +1,75 @@
import { ThirdwebAuth } from "../../core";
import { Json, LoginPayloadOutputSchema, User } from "../../core/schema";
import { GenericAuthWallet } from "@thirdweb-dev/wallets";
import { GetServerSidePropsContext, NextApiRequest } from "next";
import { NextRequest } from "next/server";
import { z } from "zod";
export const LoginPayloadBodySchema = z.object({
payload: LoginPayloadOutputSchema,
});
type RequestType =
| GetServerSidePropsContext["req"]
| NextRequest
| NextApiRequest;
export type ThirdwebAuthRoute = "login" | "logout" | "user";
export type ThirdwebAuthUser<
TData extends Json = Json,
TSession extends Json = Json,
> = User<TSession> & {
data?: TData;
};
export type ThirdwebAuthConfig<
TData extends Json = Json,
TSession extends Json = Json,
> = {
domain: string;
wallet: GenericAuthWallet;
authOptions?: {
statement?: string;
uri?: string;
version?: string;
chainId?: string;
resources?: string[];
validateNonce?:
| ((nonce: string) => void)
| ((nonce: string) => Promise<void>);
validateTokenId?:
| ((tokenId: string) => void)
| ((tokenId: string) => Promise<void>);
tokenDurationInSeconds?: number;
};
cookieOptions?: {
domain?: string;
path?: string;
sameSite?: "lax" | "strict" | "none";
};
callbacks?: {
onLogin?:
| ((address: string, req?: NextApiRequest) => void | TSession)
| ((address: string, req?: NextApiRequest) => Promise<void | TSession>);
onUser?:
| (<TRequestType extends RequestType = RequestType>(
user: User<TSession>,
req?: TRequestType,
) => void | TData)
| (<TRequestType extends RequestType = RequestType>(
user: User<TSession>,
req?: TRequestType,
) => Promise<void | TData>);
onLogout?:
| ((user: User, req?: NextApiRequest) => void)
| ((user: User, req?: NextApiRequest) => Promise<void>);
};
};
export type ThirdwebAuthContext<
TData extends Json = Json,
TSession extends Json = Json,
> = Omit<Omit<ThirdwebAuthConfig<TData, TSession>, "wallet">, "domain"> & {
auth: ThirdwebAuth;
};
+69
View File
@@ -0,0 +1,69 @@
import * as ed25519 from "@noble/ed25519";
import { Signer, Keypair, PublicKey } from "@solana/web3.js";
import type { Ecosystem, GenericAuthWallet } from "@thirdweb-dev/wallets";
import bs58 from "bs58";
import nacl from "tweetnacl";
export interface SolanaSigner {
publicKey: PublicKey;
signMessage(message: Uint8Array): Promise<Uint8Array>;
}
export class SignerWallet implements GenericAuthWallet {
type: Ecosystem = "solana";
private signer: SolanaSigner;
constructor(signer: SolanaSigner) {
this.signer = signer;
}
public async getAddress(): Promise<string> {
return this.signer.publicKey.toBase58();
}
public async signMessage(message: string): Promise<string> {
const encodedMessage = new TextEncoder().encode(message);
const signedMessage = await this.signer.signMessage(encodedMessage);
const signature = bs58.encode(signedMessage);
return signature;
}
public async verifySignature(
message: string,
signature: string,
address: string,
): Promise<boolean> {
return nacl.sign.detached.verify(
new TextEncoder().encode(message),
bs58.decode(signature),
bs58.decode(address),
);
}
}
class KeypairSigner implements SolanaSigner {
private keypair: Signer;
public publicKey: PublicKey;
constructor(keypair: Signer) {
this.keypair = keypair;
this.publicKey = keypair.publicKey;
}
public async signMessage(message: Uint8Array): Promise<Uint8Array> {
return ed25519.sync.sign(message, this.keypair.secretKey.slice(0, 32));
}
}
export class KeypairWallet extends SignerWallet {
constructor(keypair: Keypair) {
super(new KeypairSigner(keypair));
}
}
export class PrivateKeyWallet extends KeypairWallet {
constructor(privateKey: string) {
super(Keypair.fromSecretKey(bs58.decode(privateKey)));
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extension": [
"ts"
],
"require": [
"@swc-node/register"
]
}
+353
View File
@@ -0,0 +1,353 @@
import { ThirdwebAuth } from "../src/core";
import { SignerWallet } from "../src/evm";
import { expect } from "chai";
import { Wallet } from "ethers";
describe("Wallet Authentication", async () => {
let adminWallet: any, signerWallet: any, attackerWallet: any;
let auth: ThirdwebAuth;
before(async () => {
const [adminSigner, signerSigner, attackerSigner] = [
Wallet.createRandom(),
Wallet.createRandom(),
Wallet.createRandom(),
];
adminWallet = new SignerWallet(adminSigner);
signerWallet = new SignerWallet(signerSigner);
attackerWallet = new SignerWallet(attackerSigner);
auth = new ThirdwebAuth(signerWallet, "thirdweb.com");
});
beforeEach(async () => {
auth.updateWallet(signerWallet);
});
it("Should verify logged in wallet", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const address = await auth.verify(payload);
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should verify logged in wallet with chain ID and expiration", async () => {
const payload = await auth.login({
expirationTime: new Date(Date.now() + 1000 * 60 * 5),
chainId: "137",
});
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
chainId: "137",
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should verify payload with resources", async () => {
const payload = await auth.login({
resources: ["https://example.com", "https://test.com"],
});
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
resources: ["https://example.com", "https://test.com"],
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should reject payload without necessary resources", async () => {
const payload = await auth.login({
resources: ["https://example.com"],
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
resources: ["https://example.com", "https://test.com"],
});
expect.fail();
} catch (err: any) {
expect(err.message).to.equal(
"Login request is missing required resources: https://test.com",
);
}
});
it("Should verify payload with customized statement", async () => {
const payload = await auth.login({
statement: "Please sign!",
});
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
statement: "Please sign!",
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should reject payload with incorrect statement", async () => {
const payload = await auth.login({
statement: "Please sign!",
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
statement: "Please sign again!",
});
expect.fail();
} catch (err: any) {
expect(err.message).to.include(
"Expected statement 'Please sign again!' does not match statement on payload",
);
}
});
it("Should reject invalid nonce", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
validateNonce: (nonce: string) => {
if (nonce === payload.payload.nonce) {
throw new Error();
}
},
});
expect.fail();
} catch (err: any) {
expect(err.message).to.equal("Login request nonce is invalid");
}
});
it("Should accept valid nonce", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
validateNonce: (nonce: string) => {
if (nonce !== payload.payload.nonce) {
throw new Error();
}
},
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should reject payload with incorrect domain", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, { domain: "test.thirdweb.com" });
expect.fail();
} catch (err: any) {
expect(err.message).to.equal(
"Expected domain 'test.thirdweb.com' does not match domain on payload 'thirdweb.com'",
);
}
});
it("Should reject expired login payload", async () => {
const payload = await auth.login({
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload);
expect.fail();
} catch (err: any) {
expect(err.message).to.equal("Login request has expired");
}
});
it("Should reject payload with incorrect chain ID", async () => {
const payload = await auth.login({
chainId: "1",
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
chainId: "137",
});
expect.fail();
} catch (err: any) {
expect(err.message).to.equal(
"Expected chain ID '137' does not match chain ID on payload '1'",
);
}
});
it("Should reject payload with incorrect signer", async () => {
const payload = await auth.login();
payload.payload.address = await attackerWallet.getAddress();
auth.updateWallet(adminWallet);
try {
await auth.verify(payload);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("does not match payload address");
}
});
it("Should generate valid authentication token", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload);
const user = await auth.authenticate(token);
expect(user.address).to.equal(await signerWallet.getAddress());
});
it("Should reject token with incorrect domain", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload);
try {
await auth.authenticate(token, { domain: "test.thirdweb.com" });
expect.fail();
} catch (err: any) {
expect(err.message).to.contain(
"Expected token to be for the domain 'test.thirdweb.com', but found token with domain 'thirdweb.com'",
);
}
});
it("Should reject token before invalid before", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
invalidBefore: new Date(Date.now() + 1000 * 60 * 5),
});
try {
await auth.authenticate(token);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("This token is invalid before");
}
});
it("Should reject expired authentication token", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
try {
await auth.authenticate(token);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("This token expired");
}
});
it("Should reject if admin address is not connected wallet address", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload);
auth.updateWallet(signerWallet);
try {
await auth.authenticate(token);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain(
`Expected the connected wallet address '${await signerWallet.getAddress()}' to match the token issuer address '${await adminWallet.getAddress()}'`,
);
}
});
it("Should accept token with valid token ID", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
tokenId: "test",
});
const user = await auth.authenticate(token, {
validateTokenId: (tokenId: string) => {
if (tokenId !== "test") {
throw new Error();
}
},
});
expect(user.address).to.equal(await signerWallet.getAddress());
});
it("Should reject token with invalid token ID", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
tokenId: "test",
});
try {
await auth.authenticate(token, {
validateTokenId: (tokenId: string) => {
if (tokenId !== "invalid") {
throw new Error();
}
},
});
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("Token ID is invalid");
}
});
it("Should propagate session on token", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
session: { role: "admin" },
});
const user = await auth.authenticate(token);
expect(user.address).to.equal(await signerWallet.getAddress());
expect(user.session).to.deep.equal({ role: "admin" });
});
it("Should call session callback function", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
session: (address: string) => {
return { address, role: "admin" };
},
});
const user = await auth.authenticate(token);
expect(user.address).to.equal(await signerWallet.getAddress());
expect(user.session).to.deep.equal({
address: await signerWallet.getAddress(),
role: "admin",
});
});
});
+353
View File
@@ -0,0 +1,353 @@
import { ThirdwebAuth } from "../src/core";
import { KeypairWallet } from "../src/solana";
import { Keypair } from "@solana/web3.js";
import { expect } from "chai";
describe("Wallet Authentication", async () => {
let adminWallet: any, signerWallet: any, attackerWallet: any;
let auth: ThirdwebAuth;
before(async () => {
const [adminSigner, signerSigner, attackerSigner] = [
Keypair.generate(),
Keypair.generate(),
Keypair.generate(),
];
adminWallet = new KeypairWallet(adminSigner);
signerWallet = new KeypairWallet(signerSigner);
attackerWallet = new KeypairWallet(attackerSigner);
auth = new ThirdwebAuth(signerWallet, "thirdweb.com");
});
beforeEach(async () => {
auth.updateWallet(signerWallet);
});
it("Should verify logged in wallet", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const address = await auth.verify(payload);
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should verify logged in wallet with chain ID and expiration", async () => {
const payload = await auth.login({
expirationTime: new Date(Date.now() + 1000 * 60 * 5),
chainId: "137",
});
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
chainId: "137",
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should verify payload with resources", async () => {
const payload = await auth.login({
resources: ["https://example.com", "https://test.com"],
});
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
resources: ["https://example.com", "https://test.com"],
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should reject payload without necessary resources", async () => {
const payload = await auth.login({
resources: ["https://example.com"],
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
resources: ["https://example.com", "https://test.com"],
});
expect.fail();
} catch (err: any) {
expect(err.message).to.equal(
"Login request is missing required resources: https://test.com",
);
}
});
it("Should verify payload with customized statement", async () => {
const payload = await auth.login({
statement: "Please sign!",
});
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
statement: "Please sign!",
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should reject payload with incorrect statement", async () => {
const payload = await auth.login({
statement: "Please sign!",
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
statement: "Please sign again!",
});
expect.fail();
} catch (err: any) {
expect(err.message).to.include(
"Expected statement 'Please sign again!' does not match statement on payload",
);
}
});
it("Should reject invalid nonce", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
validateNonce: (nonce: string) => {
if (nonce === payload.payload.nonce) {
throw new Error();
}
},
});
expect.fail();
} catch (err: any) {
expect(err.message).to.equal("Login request nonce is invalid");
}
});
it("Should accept valid nonce", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const address = await auth.verify(payload, {
validateNonce: (nonce: string) => {
if (nonce !== payload.payload.nonce) {
throw new Error();
}
},
});
expect(address).to.equal(await signerWallet.getAddress());
});
it("Should reject payload with incorrect domain", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, { domain: "test.thirdweb.com" });
expect.fail();
} catch (err: any) {
expect(err.message).to.equal(
"Expected domain 'test.thirdweb.com' does not match domain on payload 'thirdweb.com'",
);
}
});
it("Should reject expired login payload", async () => {
const payload = await auth.login({
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload);
expect.fail();
} catch (err: any) {
expect(err.message).to.equal("Login request has expired");
}
});
it("Should reject payload with incorrect chain ID", async () => {
const payload = await auth.login({
chainId: "1",
});
auth.updateWallet(adminWallet);
try {
await auth.verify(payload, {
chainId: "137",
});
expect.fail();
} catch (err: any) {
expect(err.message).to.equal(
"Expected chain ID '137' does not match chain ID on payload '1'",
);
}
});
it("Should reject payload with incorrect signer", async () => {
const payload = await auth.login();
payload.payload.address = await attackerWallet.getAddress();
auth.updateWallet(adminWallet);
try {
await auth.verify(payload);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("does not match payload address");
}
});
it("Should generate valid authentication token", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload);
const user = await auth.authenticate(token);
expect(user.address).to.equal(await signerWallet.getAddress());
});
it("Should reject token with incorrect domain", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload);
try {
await auth.authenticate(token, { domain: "test.thirdweb.com" });
expect.fail();
} catch (err: any) {
expect(err.message).to.contain(
"Expected token to be for the domain 'test.thirdweb.com', but found token with domain 'thirdweb.com'",
);
}
});
it("Should reject token before invalid before", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
invalidBefore: new Date(Date.now() + 1000 * 60 * 5),
});
try {
await auth.authenticate(token);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("This token is invalid before");
}
});
it("Should reject expired authentication token", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
try {
await auth.authenticate(token);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("This token expired");
}
});
it("Should reject if admin address is not connected wallet address", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload);
auth.updateWallet(signerWallet);
try {
await auth.authenticate(token);
expect.fail();
} catch (err: any) {
expect(err.message).to.contain(
`Expected the connected wallet address '${await signerWallet.getAddress()}' to match the token issuer address '${await adminWallet.getAddress()}'`,
);
}
});
it("Should accept token with valid token ID", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
tokenId: "test",
});
const user = await auth.authenticate(token, {
validateTokenId: (tokenId: string) => {
if (tokenId !== "test") {
throw new Error();
}
},
});
expect(user.address).to.equal(await signerWallet.getAddress());
});
it("Should reject token with invalid token ID", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
tokenId: "test",
});
try {
await auth.authenticate(token, {
validateTokenId: (tokenId: string) => {
if (tokenId !== "invalid") {
throw new Error();
}
},
});
expect.fail();
} catch (err: any) {
expect(err.message).to.contain("Token ID is invalid");
}
});
it("Should propagate session on token", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
session: { role: "admin" },
});
const user = await auth.authenticate(token);
expect(user.address).to.equal(await signerWallet.getAddress());
expect(user.session).to.deep.equal({ role: "admin" });
});
it("Should call session callback function", async () => {
const payload = await auth.login();
auth.updateWallet(adminWallet);
const token = await auth.generate(payload, {
session: (address: string) => {
return { address, role: "admin" };
},
});
const user = await auth.authenticate(token);
expect(user.address).to.equal(await signerWallet.getAddress());
expect(user.session).to.deep.equal({
address: await signerWallet.getAddress(),
role: "admin",
});
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
{
"extends": "@thirdweb-dev/tsconfig/base.json",
"extends": "@thirdweb-dev/tsconfig/sdk.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}
+5 -1
View File
@@ -86,6 +86,7 @@
"@solana/wallet-adapter-react": "^0.15.19",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/sdk": "*",
"@thirdweb-dev/auth": "*",
"ethers": ">=5.5.1",
"react": ">=18.0.0"
},
@@ -99,6 +100,9 @@
},
"@solana/wallet-adapter-react": {
"optional": true
},
"@thirdweb-dev/auth": {
"optional": true
}
}
}
}
@@ -1,3 +1,6 @@
import { useSigner } from "../hooks/useSigner";
import { ThirdwebAuth } from "@thirdweb-dev/auth";
import { SignerWallet } from "@thirdweb-dev/auth/evm";
import React, {
PropsWithChildren,
createContext,
@@ -22,38 +25,46 @@ export interface ThirdwebAuthConfig {
* This domain should match the domain used on your auth backend.
*/
domain: string;
/**
* The URL to redirect to after a succesful login.
*/
loginRedirect?: string;
}
const ThirdwebAuthConfigContext = createContext<ThirdwebAuthConfig | undefined>(
interface ThirdwebAuthContext extends ThirdwebAuthConfig {
auth?: ThirdwebAuth;
}
const ThirdwebAuthContext = createContext<ThirdwebAuthContext | undefined>(
undefined,
);
export const ThirdwebAuthConfigProvider: React.FC<
export const ThirdwebAuthProvider: React.FC<
PropsWithChildren<{ value?: ThirdwebAuthConfig }>
> = ({ value, children }) => {
const signer = useSigner();
// Remove trailing slash from URL if present
const authConfig = useMemo(
() =>
value
? {
...value,
authUrl: value.authUrl.replace(/\/$/, ""),
}
: undefined,
[value],
);
const authContext = useMemo(() => {
if (!value) {
return undefined;
}
const context: ThirdwebAuthContext = {
...value,
authUrl: value.authUrl.replace(/\/$/, ""),
auth: undefined,
};
if (signer) {
context.auth = new ThirdwebAuth(new SignerWallet(signer), value.domain);
}
return context;
}, [value, signer]);
return (
<ThirdwebAuthConfigContext.Provider value={authConfig}>
<ThirdwebAuthContext.Provider value={authContext}>
{children}
</ThirdwebAuthConfigContext.Provider>
</ThirdwebAuthContext.Provider>
);
};
export function useThirdwebAuthConfig() {
return useContext(ThirdwebAuthConfigContext);
export function useThirdwebAuthContext() {
return useContext(ThirdwebAuthContext);
}
@@ -1,19 +1,3 @@
import { LoginConfig, useLogin } from "./useLogin";
import { useLogout } from "./useLogout";
import { useUser } from "./useUser";
export * from "./useLogin";
export * from "./useLogout";
export * from "./useUser";
/**
*
* @returns
* @internal
*/
export function useAuth(loginConfig?: LoginConfig) {
const user = useUser();
const login = useLogin(loginConfig);
const logout = useLogout();
return { ...user, login, logout };
}
@@ -1,65 +1,44 @@
import { useThirdwebAuthConfig } from "../../contexts/thirdweb-auth";
import { useSDK } from "../../providers/base";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { cacheKeys } from "../../utils/cache-keys";
import { useQueryClient } from "@tanstack/react-query";
import { LoginOptions } from "@thirdweb-dev/sdk";
import React from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { LoginOptions } from "@thirdweb-dev/auth";
import invariant from "tiny-invariant";
export interface LoginConfig {
/**
* The URL to redirect to on login.
*/
redirectTo?: string;
/**
* Function to run on error.
*/
onError?: (error: string) => void;
}
/**
* Hook to securely login to a backend with the connected wallet. The backend
* authentication URL must be configured on the ThirdwebProvider.
*
* @param config - Configuration for the login.
* @returns - A function to invoke to login with the connected wallet.
* @returns - A function to invoke to login with the connected wallet, and an isLoading state.
*
* @beta
*/
export function useLogin(config?: LoginConfig) {
const sdk = useSDK();
export function useLogin() {
const queryClient = useQueryClient();
const authConfig = useThirdwebAuthConfig();
const authConfig = useThirdwebAuthContext();
React.useEffect(() => {
const queryParams = new URLSearchParams(window.location.search);
const error = queryParams.get("error");
const login = useMutation({
mutationFn: async (options?: LoginOptions) => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
invariant(authConfig.auth, "You need a connected wallet to login.");
if (error && config?.onError) {
// If there is an error, parse it and trigger the onError callback
config.onError(decodeURI(error));
}
}, [config]);
const payload = await authConfig.auth.login(options);
await fetch(`${authConfig.authUrl}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ payload }),
});
async function login(cfg?: LoginOptions) {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
const payload = await sdk?.auth.login(authConfig.domain, cfg);
queryClient.invalidateQueries(cacheKeys.auth.user());
},
});
const encodedPayload = encodeURIComponent(btoa(JSON.stringify(payload)));
const encodedRedirectTo = encodeURIComponent(
config?.redirectTo ||
authConfig.loginRedirect ||
window.location.toString(),
);
queryClient.invalidateQueries(cacheKeys.auth.user());
// Redirect to the login URL with the encoded payload
window.location.href = `${authConfig.authUrl}/login?payload=${encodedPayload}&redirect=${encodedRedirectTo}`;
}
return login;
return {
login: (options?: LoginOptions) => login.mutateAsync(options),
isLoading: login.isLoading,
};
}
@@ -1,6 +1,6 @@
import { useThirdwebAuthConfig } from "../../contexts/thirdweb-auth";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { cacheKeys } from "../../utils/cache-keys";
import { useQueryClient } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import invariant from "tiny-invariant";
/**
@@ -13,16 +13,22 @@ import invariant from "tiny-invariant";
*/
export function useLogout() {
const queryClient = useQueryClient();
const authConfig = useThirdwebAuthConfig();
const authConfig = useThirdwebAuthContext();
function logout() {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
queryClient.invalidateQueries(cacheKeys.auth.user());
window.location.href = `${authConfig.authUrl}/logout`;
}
const logout = useMutation({
mutationFn: async () => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
return logout;
await fetch(`${authConfig.authUrl}/logout`, {
method: "POST",
});
queryClient.invalidateQueries(cacheKeys.auth.user());
},
});
return { logout: logout.mutateAsync, isLoading: logout.isLoading };
}
@@ -1,10 +1,14 @@
import { useThirdwebAuthConfig } from "../../contexts/thirdweb-auth";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { cacheKeys } from "../../utils/cache-keys";
import { useQuery } from "@tanstack/react-query";
import { Json, User } from "@thirdweb-dev/auth";
import invariant from "tiny-invariant";
export interface ThirdwebAuthUser {
address: string;
export interface UserWithData<
TData extends Json = Json,
TContext extends Json = Json,
> extends User<TContext> {
data?: TData;
}
/**
@@ -14,8 +18,11 @@ export interface ThirdwebAuthUser {
*
* @beta
*/
export function useUser() {
const authConfig = useThirdwebAuthConfig();
export function useUser<
TData extends Json = Json,
TContext extends Json = Json,
>() {
const authConfig = useThirdwebAuthContext();
const { data: user, isLoading } = useQuery(
cacheKeys.auth.user(),
@@ -24,15 +31,18 @@ export function useUser() {
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
// We include credentials so we can getUser even if API is on different URL
const res = await fetch(`${authConfig.authUrl}/user`, {
credentials: "include",
});
return (await res.json()) as ThirdwebAuthUser;
return (await res.json()) as UserWithData<TData, TContext>;
},
{
enabled: !!authConfig,
},
);
return { user, isLoading };
return { user, isLoggedIn: !!user, isLoading };
}
@@ -6,7 +6,7 @@ import { RequiredParam } from "../../core/query-utils/required-param";
import { ComponentWithChildren } from "../../core/types/component";
import {
ThirdwebAuthConfig,
ThirdwebAuthConfigProvider,
ThirdwebAuthProvider,
} from "../contexts/thirdweb-auth";
import {
ThirdwebConnectedWalletProvider,
@@ -102,11 +102,11 @@ export const WrappedThirdwebSDKProvider: ComponentWithChildren<
return (
<QueryClientProviderWithDefault queryClient={queryClient}>
<ThirdwebAuthConfigProvider value={authConfig}>
<ThirdwebAuthProvider value={authConfig}>
<ThirdwebSDKContext.Provider value={ctxValue}>
{children}
</ThirdwebSDKContext.Provider>
</ThirdwebAuthConfigProvider>
</ThirdwebAuthProvider>
</QueryClientProviderWithDefault>
);
};
@@ -1,3 +1,6 @@
import { ThirdwebAuth } from "@thirdweb-dev/auth";
import { SignerWallet } from "@thirdweb-dev/auth/solana";
import { ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
import React, {
PropsWithChildren,
createContext,
@@ -24,36 +27,48 @@ export interface ThirdwebAuthConfig {
domain: string;
/**
* The URL to redirect to after a succesful login.
* Solana SDK
*/
loginRedirect?: string;
sdk?: ThirdwebSDK;
}
const ThirdwebAuthConfigContext = createContext<ThirdwebAuthConfig | undefined>(
interface ThirdwebAuthContext extends ThirdwebAuthConfig {
auth?: ThirdwebAuth;
}
const ThirdwebAuthContext = createContext<ThirdwebAuthContext | undefined>(
undefined,
);
export const ThirdwebAuthConfigProvider: React.FC<
export const ThirdwebAuthProvider: React.FC<
PropsWithChildren<{ value?: ThirdwebAuthConfig }>
> = ({ value, children }) => {
// Remove trailing slash from URL if present
const authConfig = useMemo(
() =>
value
? {
...value,
authUrl: value.authUrl.replace(/\/$/, ""),
}
: undefined,
[value],
);
const authContext = useMemo(() => {
if (!value) {
return undefined;
}
const context: ThirdwebAuthContext = {
...value,
authUrl: value.authUrl.replace(/\/$/, ""),
auth: undefined,
};
const identity = value.sdk?.wallet.getSigner();
if (identity) {
context.auth = new ThirdwebAuth(new SignerWallet(identity), value.domain);
}
return context;
}, [value]);
return (
<ThirdwebAuthConfigContext.Provider value={authConfig}>
<ThirdwebAuthContext.Provider value={authContext}>
{children}
</ThirdwebAuthConfigContext.Provider>
</ThirdwebAuthContext.Provider>
);
};
export function useThirdwebAuthConfig() {
return useContext(ThirdwebAuthConfigContext);
export function useThirdwebAuthContext() {
return useContext(ThirdwebAuthContext);
}
@@ -1,19 +1,3 @@
import { LoginConfig, useLogin } from "./useLogin";
import { useLogout } from "./useLogout";
import { useUser } from "./useUser";
export * from "./useLogin";
export * from "./useLogout";
export * from "./useUser";
/**
*
* @returns
* @internal
*/
export function useAuth(loginConfig?: LoginConfig) {
const user = useUser();
const login = useLogin(loginConfig);
const logout = useLogout();
return { ...user, login, logout };
}
@@ -1,9 +1,7 @@
import { ensureTWPrefix } from "../../../core/query-utils/query-key";
import { useThirdwebAuthConfig } from "../../contexts/thirdweb-auth";
import { useSDK } from "../../providers/base";
import { useQueryClient } from "@tanstack/react-query";
import { LoginOptions } from "@thirdweb-dev/sdk";
import React from "react";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { LoginOptions } from "@thirdweb-dev/auth";
import invariant from "tiny-invariant";
export interface LoginConfig {
@@ -26,40 +24,33 @@ export interface LoginConfig {
*
* @beta
*/
export function useLogin(config?: LoginConfig) {
const sdk = useSDK();
export function useLogin() {
const queryClient = useQueryClient();
const authConfig = useThirdwebAuthConfig();
const authConfig = useThirdwebAuthContext();
React.useEffect(() => {
const queryParams = new URLSearchParams(window.location.search);
const error = queryParams.get("error");
const login = useMutation({
mutationFn: async (options?: LoginOptions) => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
invariant(authConfig.auth, "You need a connected wallet to login.");
if (error && config?.onError) {
// If there is an error, parse it and trigger the onError callback
config.onError(decodeURI(error));
}
}, [config]);
const payload = await authConfig.auth.login(options);
await fetch(`${authConfig.authUrl}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ payload }),
});
async function login(cfg?: LoginOptions) {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
const payload = await sdk?.auth.login(authConfig.domain, cfg);
queryClient.invalidateQueries(ensureTWPrefix(["user"]));
},
});
const encodedPayload = encodeURIComponent(btoa(JSON.stringify(payload)));
const encodedRedirectTo = encodeURIComponent(
config?.redirectTo ||
authConfig.loginRedirect ||
window.location.toString(),
);
queryClient.invalidateQueries(ensureTWPrefix(["user"]));
// Redirect to the login URL with the encoded payload
window.location.href = `${authConfig.authUrl}/login?payload=${encodedPayload}&redirect=${encodedRedirectTo}`;
}
return login;
return {
login: (options?: LoginOptions) => login.mutateAsync(options),
isLoading: login.isLoading,
};
}
@@ -1,6 +1,6 @@
import { ensureTWPrefix } from "../../../core/query-utils/query-key";
import { useThirdwebAuthConfig } from "../../contexts/thirdweb-auth";
import { useQueryClient } from "@tanstack/react-query";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import invariant from "tiny-invariant";
/**
@@ -13,16 +13,22 @@ import invariant from "tiny-invariant";
*/
export function useLogout() {
const queryClient = useQueryClient();
const authConfig = useThirdwebAuthConfig();
const authConfig = useThirdwebAuthContext();
function logout() {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
queryClient.invalidateQueries(ensureTWPrefix(["user"]));
window.location.href = `${authConfig.authUrl}/logout`;
}
const logout = useMutation({
mutationFn: async () => {
invariant(
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
return logout;
await fetch(`${authConfig.authUrl}/logout`, {
method: "POST",
});
queryClient.invalidateQueries(ensureTWPrefix(["user"]));
},
});
return { logout: logout.mutateAsync, isLoading: logout.isLoading };
}
@@ -1,10 +1,14 @@
import { ensureTWPrefix } from "../../../core/query-utils/query-key";
import { useThirdwebAuthConfig } from "../../contexts/thirdweb-auth";
import { useThirdwebAuthContext } from "../../contexts/thirdweb-auth";
import { useQuery } from "@tanstack/react-query";
import { Json, User } from "@thirdweb-dev/auth";
import invariant from "tiny-invariant";
export interface ThirdwebAuthUser {
address: string;
export interface UserWithData<
TData extends Json = Json,
TContext extends Json = Json,
> extends User<TContext> {
data?: TData;
}
/**
@@ -14,8 +18,11 @@ export interface ThirdwebAuthUser {
*
* @beta
*/
export function useUser() {
const authConfig = useThirdwebAuthConfig();
export function useUser<
TData extends Json = Json,
TContext extends Json = Json,
>() {
const authConfig = useThirdwebAuthContext();
const { data: user, isLoading } = useQuery(
ensureTWPrefix(["user"]),
@@ -24,15 +31,18 @@ export function useUser() {
authConfig,
"Please specify an authConfig in the ThirdwebProvider",
);
// We include credentials so we can getUser even if API is on different URL
const res = await fetch(`${authConfig.authUrl}/user`, {
credentials: "include",
});
return (await res.json()) as ThirdwebAuthUser;
return (await res.json()) as UserWithData<TData, TContext>;
},
{
enabled: !!authConfig,
},
);
return { user, isLoading };
return { user, isLoggedIn: !!user, isLoading };
}
@@ -6,7 +6,7 @@ import { RequiredParam } from "../../core/query-utils/required-param";
import { ComponentWithChildren } from "../../core/types/component";
import {
ThirdwebAuthConfig,
ThirdwebAuthConfigProvider,
ThirdwebAuthProvider,
} from "../contexts/thirdweb-auth";
import type { WalletContextState } from "@solana/wallet-adapter-react";
import { Network, ThirdwebSDK } from "@thirdweb-dev/sdk/solana";
@@ -78,13 +78,21 @@ export const ThirdwebSDKProvider: ComponentWithChildren<
[sdk, network],
);
const authConfigValue = useMemo(() => {
if (!authConfig) {
return undefined;
}
return { ...authConfig, sdk: sdk || undefined };
}, [authConfig, sdk]);
return (
<QueryClientProviderWithDefault queryClient={queryClient}>
<ThirdwebAuthConfigProvider value={authConfig}>
<ThirdwebAuthProvider value={authConfigValue}>
<ThirdwebSDKContext.Provider value={ctxValue}>
{children}
</ThirdwebSDKContext.Provider>
</ThirdwebAuthConfigProvider>
</ThirdwebAuthProvider>
</QueryClientProviderWithDefault>
);
};
+2 -1
View File
@@ -76,6 +76,7 @@
"@solana/wallet-adapter-wallets": "^0.19.0",
"@solana/wallet-adapter-phantom": "^0.9.17",
"@solana/web3.js": "^1.62.0",
"@thirdweb-dev/auth": "*",
"@thirdweb-dev/sdk": "*",
"@types/color": "^3.0.3",
"@types/mime": "^3.0.1",
@@ -148,4 +149,4 @@
"optional": true
}
}
}
}
@@ -21,15 +21,17 @@ import { FiLock } from "@react-icons/all-files/fi/FiLock";
import { FiShuffle } from "@react-icons/all-files/fi/FiShuffle";
import { FiWifi } from "@react-icons/all-files/fi/FiWifi";
import { FiXCircle } from "@react-icons/all-files/fi/FiXCircle";
import type { LoginOptions } from "@thirdweb-dev/auth";
import {
useThirdwebAuthConfig,
LoginConfig,
useAuth,
useThirdwebAuthContext,
useAddress,
useBalance,
useChainId,
useLogin,
useLogout,
useUser,
} from "@thirdweb-dev/react-core/evm";
import { ChainId, LoginOptions, SUPPORTED_CHAIN_ID } from "@thirdweb-dev/sdk";
import { ChainId, SUPPORTED_CHAIN_ID } from "@thirdweb-dev/sdk";
import * as menu from "@zag-js/menu";
import { normalizeProps, useMachine } from "@zag-js/react";
import React, { useId } from "react";
@@ -64,7 +66,6 @@ function getIconForConnector(connector: Connector) {
interface ConnectWalletProps extends ThemeProviderProps {
auth?: {
loginOptions?: LoginOptions;
loginConfig?: LoginConfig;
loginOptional?: boolean;
};
className?: string;
@@ -166,8 +167,10 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = ({
const { onCopy, hasCopied } = useClipboard(walletAddress || "");
const authConfig = useThirdwebAuthConfig();
const { user, isLoading, login, logout } = useAuth(auth?.loginConfig);
const authConfig = useThirdwebAuthContext();
const { user, isLoading } = useUser();
const { login } = useLogin();
const { logout } = useLogout();
const requiresSignIn = auth?.loginOptional
? false
@@ -113,11 +113,31 @@ export class GnosisSafeConnector extends Connector {
safeAddress,
});
const service = new safeEthersAdapters.SafeService(serverUrl);
return new safeEthersAdapters.SafeEthersSigner(
const safeSigner = new safeEthersAdapters.SafeEthersSigner(
safe as any,
service,
signer.provider,
);
// See this test for more details:
// https://github.com/safe-global/safe-contracts/blob/9d188d3ef514fb7391466a6b5f010db4cc0f3c8b/test/handlers/CompatibilityFallbackHandler.spec.ts#L86-L94
safeSigner.signMessage = async (message: string | ethers.utils.Bytes) => {
const EIP712_SAFE_MESSAGE_TYPE = {
SafeMessage: [{ type: "bytes", name: "message" }],
};
const encodedMessage = ethers.utils._TypedDataEncoder.hash(
{ verifyingContract: safeAddress, chainId: await this.getChainId() },
EIP712_SAFE_MESSAGE_TYPE,
{ message: ethers.utils.hashMessage(message) },
);
const safeMessage = ethers.utils.arrayify(encodedMessage);
const signature = await signer.signMessage(safeMessage);
return signature.replace(/1b$/, "1f").replace(/1c$/, "20");
};
return safeSigner;
}
async disconnect(): Promise<void> {
@@ -40,7 +40,7 @@ export function useMetamask() {
const [connectors, connect] = useConnect();
const isMetaMaskInjected =
typeof window !== "undefined" && window.ethereum?.isMetaMask;
typeof window !== "undefined" && (window.ethereum as any)?.isMetaMask;
const shouldUseWalletConnect = isMobile() && !isMetaMaskInjected;
-4
View File
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-sdk-evm-wallets.cjs.js",
"module": "dist/thirdweb-dev-sdk-evm-wallets.esm.js"
}
+2 -9
View File
@@ -19,10 +19,6 @@
"module": "./solana/dist/thirdweb-dev-sdk-solana.esm.js",
"default": "./solana/dist/thirdweb-dev-sdk-solana.cjs.js"
},
"./evm/wallets": {
"module": "./evm/wallets/dist/thirdweb-dev-sdk-evm-wallets.esm.js",
"default": "./evm/wallets/dist/thirdweb-dev-sdk-evm-wallets.cjs.js"
},
"./solana/server": {
"module": "./solana/server/dist/thirdweb-dev-sdk-solana-server.esm.js",
"default": "./solana/server/dist/thirdweb-dev-sdk-solana-server.cjs.js"
@@ -32,7 +28,6 @@
"files": [
"dist/",
"evm/",
"wallets/",
"solana/",
"server/"
],
@@ -40,7 +35,6 @@
"entrypoints": [
"index.ts",
"evm/index.ts",
"evm/wallets/index.ts",
"solana/index.ts",
"solana/server/index.ts"
],
@@ -71,7 +65,6 @@
"push": "yalc push"
},
"devDependencies": {
"@aws-sdk/client-secrets-manager": "^3.216.0",
"@metaplex-foundation/amman": "^0.12.0",
"@metaplex-foundation/amman-client": "^0.2.1",
"@metaplex-foundation/js": "^0.17.6",
@@ -86,6 +79,7 @@
"@preconstruct/cli": "^2.2.1",
"@swc-node/register": "^1.5.1",
"@swc/core": "^1.2.177",
"@thirdweb-dev/wallets": "*",
"@types/chai": "^4.3.3",
"@types/deep-equal-in-any-order": "^1.0.1",
"@types/mocha": "^10.0.0",
@@ -102,7 +96,6 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-tsdoc": "^0.2.16",
"ethers": "^5.7.2",
"ethers-aws-kms-signer": "^1.3.2",
"hardhat": "^2.9.3",
"mocha": "^10.0.0",
"prettier": "^2.7.1",
@@ -166,4 +159,4 @@
"url": "https://github.com/thirdweb-dev/js/issues"
},
"author": "thirdweb eng <[email protected]>"
}
}
-1
View File
@@ -1 +0,0 @@
export * from "./wallet-authenticator";
@@ -1,330 +0,0 @@
import { isBrowser } from "../../common/utils";
import { SDKOptions } from "../../schema";
import {
LoginOptions,
LoginPayload,
AuthenticationOptions,
LoginPayloadData,
LoginPayloadDataSchema,
AuthenticationPayloadDataSchema,
AuthenticationPayloadData,
LoginOptionsSchema,
VerifyOptionsSchema,
VerifyOptions,
AuthenticationOptionsSchema,
} from "../../schema/auth";
import { RPCConnectionHandler } from "../classes/rpc-connection-handler";
import { NetworkOrSignerOrProvider } from "../types";
import { UserWallet } from "../wallet";
/**
* Wallet Authenticator
* @remarks The wallet authenticator enables server-side applications to securely identify the
* connected wallet address of users on the client-side, and also enables users to authenticate
* to any backend using just their wallet. It implements the JSON Web Token (JWT) authentication
* standard.
*
* @example
* ```javascript
* // We specify the domain of the application to authenticate to
* const domain = "example.com"
*
* // On the client side, we can generate a payload for the connected wallet to login
* const loginPayload = await sdk.auth.login(domain);
*
* // Then on the server side, we can securely verify the connected client-side address
* const address = sdk.auth.verify(domain, loginPayload);
*
* // And we can also generate an authentication token to send to the client
* const token = sdk.auth.generate(domain, loginPayload);
*
* // Finally, the token can be send from the client to the server to make authenticated requests
* // And the server can use the following function to authenticate a token and verify the associated address
* const address = sdk.auth.authenticate(domain, token);
* ```
* @public
*/
export class WalletAuthenticator extends RPCConnectionHandler {
private wallet: UserWallet;
constructor(
network: NetworkOrSignerOrProvider,
wallet: UserWallet,
options: SDKOptions,
) {
super(network, options);
this.wallet = wallet;
}
/**
* Login With Connected Wallet
* @remarks Client-side function that allows the connected wallet to login to a server-side application.
* Generates a login payload that can be sent to the server-side for verification or authentication.
*
* @param domain - The domain of the server-side application to login to
* @param options - Optional configuration options for the login request
* @returns Login payload that can be used on the server-side to verify the login request or authenticate
*
* @example
* ```javascript
* // Add the domain of the application users will login to, this will be used throughout the login process
* const domain = "example.com";
* // Generate a signed login payload for the connected wallet to authenticate with
* const loginPayload = await sdk.auth.login(domain);
* ```
*/
public async login(
domain: string,
options?: LoginOptions,
): Promise<LoginPayload> {
const parsedOptions = LoginOptionsSchema.parse(options);
const signerAddress = await this.wallet.getAddress();
const expirationTime =
parsedOptions?.expirationTime || new Date(Date.now() + 1000 * 60 * 5);
const payloadData = LoginPayloadDataSchema.parse({
domain,
address: signerAddress,
nonce: parsedOptions?.nonce,
expiration_time: expirationTime,
chain_id: parsedOptions?.chainId,
});
const message = this.generateMessage(payloadData);
const signature = await this.wallet.sign(message);
return {
payload: payloadData,
signature,
};
}
/**
* Verify Logged In Address
* @remarks Server-side function to securely verify the address of the logged in client-side wallet
* by validating the provided client-side login request.
*
* @param domain - The domain of the server-side application to verify the login request for
* @param payload - The login payload to verify
* @returns Address of the logged in wallet
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
*
* // Verify the login request
* const address = sdk.auth.verify(domain, loginPayload);
* ```
*/
public verify(
domain: string,
payload: LoginPayload,
options?: VerifyOptions,
): string {
const parsedOptions = VerifyOptionsSchema.parse(options);
// Check that the intended domain matches the domain of the payload
if (payload.payload.domain !== domain) {
throw new Error(
`Expected domain '${domain}' does not match domain on payload '${payload.payload.domain}'`,
);
}
// Check that the payload hasn't expired
const currentTime = new Date();
if (currentTime > new Date(payload.payload.expiration_time)) {
throw new Error(`Login request has expired`);
}
// If chain ID is specified, check that it matches the chain ID of the signature
if (
parsedOptions?.chainId !== undefined &&
parsedOptions.chainId !== payload.payload.chain_id
) {
throw new Error(
`Chain ID '${parsedOptions.chainId}' does not match payload chain ID '${payload.payload.chain_id}'`,
);
}
// Check that the signing address is the claimed wallet address
const message = this.generateMessage(payload.payload);
const userAddress = this.wallet.recoverAddress(message, payload.signature);
if (userAddress.toLowerCase() !== payload.payload.address.toLowerCase()) {
throw new Error(
`Signer address '${userAddress.toLowerCase()}' does not match payload address '${payload.payload.address.toLowerCase()}'`,
);
}
return userAddress;
}
/**
* Generate Authentication Token
* @remarks Server-side function that generates a JWT token from the provided login request that the
* client-side wallet can use to authenticate to the server-side application.
*
* @param domain - The domain of the server-side application to authenticate to
* @param payload - The login payload to authenticate with
* @param options - Optional configuration options for the authentication request
* @returns A authentication token that can be used by the client to make authenticated requests
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
*
* // Generate a JWT token that can be sent to the client-side wallet and used for authentication
* const token = await sdk.auth.generateAuthToken(domain, loginPayload);
* ```
*/
public async generateAuthToken(
domain: string,
payload: LoginPayload,
options?: AuthenticationOptions,
): Promise<string> {
if (isBrowser()) {
throw new Error(
"Authentication tokens should not be generated in the browser, as they must be signed by a server-side admin wallet.",
);
}
const parsedOptions = AuthenticationOptionsSchema.parse(options);
const userAddress = this.verify(domain, payload);
const adminAddress = await this.wallet.getAddress();
const payloadData = AuthenticationPayloadDataSchema.parse({
iss: adminAddress,
sub: userAddress,
aud: domain,
nbf: parsedOptions?.invalidBefore || new Date(),
exp:
parsedOptions?.expirationTime ||
new Date(Date.now() + 1000 * 60 * 60 * 5),
iat: new Date(),
});
const message = JSON.stringify(payloadData);
const signature = await this.wallet.sign(message);
// Header used for JWT token specifying hash algorithm
const header = {
// Specify ECDSA with SHA-256 for hashing algorithm
alg: "ES256",
typ: "JWT",
};
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
"base64",
);
const encodedData = Buffer.from(JSON.stringify(payloadData))
.toString("base64")
.replace(/=/g, "");
const encodedSignature = Buffer.from(signature).toString("base64");
// Generate a JWT token with base64 encoded header, payload, and signature
const token = `${encodedHeader}.${encodedData}.${encodedSignature}`;
return token;
}
/**
* Authenticate With Token
* @remarks Server-side function that authenticates the provided JWT token. This function verifies that
* the provided authentication token is valid and returns the address of the authenticated wallet.
*
* @param domain - The domain of the server-side application doing authentication
* @param token - The authentication token being used
* @returns The address of the authenticated wallet
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
* const token = await sdk.auth.generateAuthToken(domain, loginPayload);
*
* // Authenticate the token and get the address of authenticating users wallet
* const address = sdk.auth.authenticate(domain, token);
* ```
*/
public async authenticate(domain: string, token: string): Promise<string> {
if (isBrowser()) {
throw new Error(
"Should not authenticate tokens in the browser, as they must be verified by the server-side admin wallet.",
);
}
const encodedPayload = token.split(".")[1];
const encodedSignature = token.split(".")[2];
const payload: AuthenticationPayloadData = JSON.parse(
Buffer.from(encodedPayload, "base64").toString(),
);
const signature = Buffer.from(encodedSignature, "base64").toString();
// Check that the token audience matches the domain
if (payload.aud !== domain) {
throw new Error(
`Expected token to be for the domain '${domain}', but found token with domain '${payload.aud}'`,
);
}
// Check that the token is past the invalid before time
const currentTime = Math.floor(new Date().getTime() / 1000);
if (currentTime < payload.nbf) {
throw new Error(
`This token is invalid before epoch time '${payload.nbf}', current epoch time is '${currentTime}'`,
);
}
// Check that the token hasn't expired
if (currentTime > payload.exp) {
throw new Error(
`This token expired at epoch time '${payload.exp}', current epoch time is '${currentTime}'`,
);
}
// Check that the connected wallet matches the token issuer
const connectedAddress = await this.wallet.getAddress();
if (connectedAddress.toLowerCase() !== payload.iss.toLowerCase()) {
throw new Error(
`Expected the connected wallet address '${connectedAddress}' to match the token issuer address '${payload.iss}'`,
);
}
// Check that the connected wallet signed the token
const adminAddress = this.wallet.recoverAddress(
JSON.stringify(payload),
signature,
);
if (connectedAddress.toLowerCase() !== adminAddress.toLowerCase()) {
throw new Error(
`The connected wallet address '${connectedAddress}' did not sign the token`,
);
}
return payload.sub;
}
/**
* Generates a EIP-4361 compliant message to sign based on the login payload
*/
private generateMessage(payload: LoginPayloadData): string {
let message = ``;
// Add the domain and login address for transparency
message += `${payload.domain} wants you to sign in with your account:\n${payload.address}\n\n`;
// Prompt user to make sure domain is correct to prevent phishing attacks
message += `Make sure that the requesting domain above matches the URL of the current website.\n\n`;
// Add data fields in compliance with the EIP-4361 standard
if (payload.chain_id) {
message += `Chain ID: ${payload.chain_id}\n`;
}
message += `Nonce: ${payload.nonce}\n`;
message += `Expiration Time: ${payload.expiration_time}\n`;
return message;
}
}
-1
View File
@@ -1,5 +1,4 @@
export * from "./types";
export * from "./auth";
export * from "./classes";
export * from "./wallet";
export * from "./sdk";
+16 -10
View File
@@ -14,8 +14,6 @@ import { SmartContract } from "../contracts/smart-contract";
import { AbiSchema } from "../schema";
import { SDKOptions } from "../schema/sdk-options";
import { CurrencyValue } from "../types/index";
import type { AbstractWallet } from "../wallets";
import { WalletAuthenticator } from "./auth/wallet-authenticator";
import type { ContractMetadata } from "./classes";
import { ContractDeployer } from "./classes/contract-deployer";
import { ContractPublisher } from "./classes/contract-publisher";
@@ -35,6 +33,7 @@ import type {
import { UserWallet } from "./wallet/UserWallet";
import IThirdwebContractABI from "@thirdweb-dev/contracts-js/dist/abis/IThirdwebContract.json";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import type { EVMWallet } from "@thirdweb-dev/wallets";
import { Contract, ContractInterface, ethers, Signer } from "ethers";
import invariant from "tiny-invariant";
@@ -63,7 +62,7 @@ export class ThirdwebSDK extends RPCConnectionHandler {
* @beta
*/
static async fromWallet(
wallet: AbstractWallet,
wallet: EVMWallet,
network: ChainOrRpc,
options: SDKOptions = {},
storage: ThirdwebStorage = new ThirdwebStorage(),
@@ -75,7 +74,11 @@ export class ThirdwebSDK extends RPCConnectionHandler {
? getReadOnlyProvider(signerOrProvider)
: signerOrProvider;
const signer = await wallet.getSigner(provider);
let signer = await wallet.getSigner();
if (!!provider) {
signer = signer.connect(provider);
}
return ThirdwebSDK.fromSigner(signer, network, options, storage);
}
@@ -177,10 +180,6 @@ export class ThirdwebSDK extends RPCConnectionHandler {
* Upload and download files from IPFS or from your own storage service
*/
public storage: ThirdwebStorage;
/**
* Enable authentication with the connected wallet
*/
public auth: WalletAuthenticator;
constructor(
network: ChainOrRpc | SignerOrProvider,
@@ -193,7 +192,6 @@ export class ThirdwebSDK extends RPCConnectionHandler {
this.storage = storage;
this.wallet = new UserWallet(signerOrProvider, options);
this.deployer = new ContractDeployer(signerOrProvider, options, storage);
this.auth = new WalletAuthenticator(signerOrProvider, this.wallet, options);
this.multiChainRegistry = new MultichainRegistry(
signerOrProvider,
this.storageHandler,
@@ -206,6 +204,15 @@ export class ThirdwebSDK extends RPCConnectionHandler {
);
}
get auth() {
throw new Error(
`The sdk.auth namespace has been moved to the @thirdweb-dev/auth package and is no longer available after @thirdweb-dev/sdk >= 3.7.0.
Please visit https://portal.thirdweb.com/auth for instructions on how to switch to using the new auth package (@thirdweb-dev/[email protected]).
If you still want to use the old @thirdweb-dev/[email protected] package, you can downgrade the SDK to version 3.6.0.`,
);
}
/**
* Get an instance of a NFT Drop contract
* @param contractAddress - the address of the deployed contract
@@ -584,7 +591,6 @@ export class ThirdwebSDK extends RPCConnectionHandler {
private updateContractSignerOrProvider() {
this.wallet.connect(this.getSignerOrProvider());
this.auth.updateSignerOrProvider(this.getSignerOrProvider());
this.deployer.updateSignerOrProvider(this.getSignerOrProvider());
this._publisher.updateSignerOrProvider(this.getSignerOrProvider());
this.multiChainRegistry.updateSigner(this.getSignerOrProvider());
-1
View File
@@ -12,7 +12,6 @@ export * from "./constants/chains";
export * from "./schema/tokens/token";
export * from "./schema/tokens/edition";
export * from "./schema/contracts/common";
export * from "./schema/auth";
export type {
SDKOptions,
SDKOptionsSchema,
-178
View File
@@ -1,178 +0,0 @@
import { AddressSchema, RawDateSchema } from "./shared";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
/**
* @internal
*/
export const LoginOptionsSchema = z
.object({
/**
* The optional nonce of the login request used to prevent replay attacks
*/
nonce: z.string().optional(),
/**
* The optional time after which the login payload will be invalid
*/
expirationTime: z.date().optional(),
/**
* The optional chain ID that the login request was intended for
*/
chainId: z.number().optional(),
})
.optional();
/**
* @internal
*/
export const LoginPayloadDataSchema = z.object({
/**
* The domain that the user is attempting to login to
*/
domain: z.string(),
/**
* The address of the account that is logging in
*/
address: AddressSchema,
/**
* The nonce of the login request used to prevent replay attacks, defaults to a random UUID
*/
nonce: z.string().default(uuidv4()),
/**
* The time after which the login payload will be invalid, defaults to 5 minutes from now
*/
expiration_time: z.date().transform((d) => d.toISOString()),
/**
* The chain ID that the login request was intended for, defaults to none
*/
chain_id: z.number().optional(),
});
/**
* @internal
*/
export const LoginPayloadSchema = z.object({
/**
* The payload data used for login
*/
payload: LoginPayloadDataSchema,
/**
* The signature of the login request used for verification
*/
signature: z.string(),
});
/**
* @internal
*/
export const VerifyOptionsSchema = z
.object({
/**
* The optional chain ID to expect the request to be for
*/
chainId: z.number().optional(),
})
.optional();
/**
* @internal
*/
export const AuthenticationOptionsSchema = z
.object({
/**
* The date before which the authentication payload is invalid
*/
invalidBefore: z.date().optional(),
/**
* The date after which the authentication payload is invalid
*/
expirationTime: z.date().optional(),
})
.optional();
/**
* @internal
*/
export const AuthenticationPayloadDataSchema = z.object({
/**
* The address of the wallet issuing the payload
*/
iss: z.string(),
/**
* The address of the wallet requesting to authenticate
*/
sub: z.string(),
/**
* The domain intended to receive the authentication payload
*/
aud: z.string(),
/**
* The date before which the authentication payload is invalid
*/
exp: RawDateSchema.transform((b) => b.toNumber()),
/**
* The date after which the authentication payload is invalid
*/
nbf: RawDateSchema.transform((b) => b.toNumber()),
/**
* The date on which the payload was issued
*/
iat: RawDateSchema.transform((b) => b.toNumber()),
/**
* The unique identifier of the payload
*/
jti: z.string().default(uuidv4()),
});
/**
* @internal
*/
export const AuthenticationPayloadSchema = z.object({
/**
* The payload data used for authentication
*/
payload: AuthenticationPayloadDataSchema,
/**
* The signature of the authentication payload used for authentication
*/
signature: z.string(),
});
/**
* @public
*/
export type LoginOptions = z.input<typeof LoginOptionsSchema>;
/**
* @public
*/
export type LoginPayloadData = z.output<typeof LoginPayloadDataSchema>;
/**
* @public
*/
export type LoginPayload = z.output<typeof LoginPayloadSchema>;
/**
* @public
*/
export type VerifyOptions = z.input<typeof VerifyOptionsSchema>;
/**
* @public
*/
export type AuthenticationOptions = z.input<typeof AuthenticationOptionsSchema>;
/**
* @public
*/
export type AuthenticationPayloadData = z.output<
typeof AuthenticationPayloadDataSchema
>;
/**
* @public
*/
export type AuthenticationPayload = z.output<
typeof AuthenticationPayloadSchema
>;
-1
View File
@@ -2,4 +2,3 @@ export * from "./contracts";
export * from "./tokens";
export * from "./sdk-options";
export * from "./shared";
export * from "./auth";
@@ -1,2 +0,0 @@
export * from "./aws-secrets-manager";
export * from "./aws-kms";
-2
View File
@@ -1,2 +0,0 @@
export * from "./core";
export * from "./types";
@@ -1,7 +0,0 @@
import { ethers } from "ethers";
export abstract class AbstractWallet {
abstract getSigner(
provider?: ethers.providers.Provider,
): Promise<ethers.Signer>;
}
@@ -1 +0,0 @@
export * from "./abstract";
@@ -1,303 +0,0 @@
import {
AuthenticationOptions,
AuthenticationOptionsSchema,
AuthenticationPayloadData,
AuthenticationPayloadDataSchema,
LoginOptions,
LoginOptionsSchema,
LoginPayload,
LoginPayloadData,
LoginPayloadDataSchema,
} from "../types/auth";
import { UserWallet } from "./user-wallet";
import { isBrowser } from "@thirdweb-dev/storage";
/**
* Wallet Authenticator
* @remarks The wallet authenticator enables server-side applications to securely identify the
* connected wallet address of users on the client-side, and also enables users to authenticate
* to any backend using just their wallet. It implements the JSON Web Token (JWT) authentication
* standard.
*
* @example
* ```javascript
* // We specify the domain of the application to authenticate to
* const domain = "example.com"
*
* // On the client side, we can generate a payload for the connected wallet to login
* const loginPayload = await sdk.auth.login(domain);
*
* // Then on the server side, we can securely verify the connected client-side address
* const address = sdk.auth.verify(domain, loginPayload);
*
* // And we can also generate an authentication token to send to the client
* const token = sdk.auth.generate(domain, loginPayload);
*
* // Finally, the token can be send from the client to the server to make authenticated requests
* // And the server can use the following function to authenticate a token and verify the associated address
* const address = sdk.auth.authenticate(domain, token);
* ```
* @public
*/
export class WalletAuthenticator {
private wallet: UserWallet;
constructor(wallet: UserWallet) {
this.wallet = wallet;
}
/**
* Login With Connected Wallet
* @remarks Client-side function that allows the connected wallet to login to a server-side application.
* Generates a login payload that can be sent to the server-side for verification or authentication.
*
* @param domain - The domain of the server-side application to login to
* @param options - Optional configuration options for the login request
* @returns Login payload that can be used on the server-side to verify the login request or authenticate
*
* @example
* ```javascript
* // Add the domain of the application users will login to, this will be used throughout the login process
* const domain = "example.com";
* // Generate a signed login payload for the connected wallet to authenticate with
* const loginPayload = await sdk.auth.login(domain);
* ```
*/
public async login(
domain: string,
options?: LoginOptions,
): Promise<LoginPayload> {
const parsedOptions = LoginOptionsSchema.parse(options);
const signerAddress = this.wallet.getAddress();
const expirationTime =
parsedOptions?.expirationTime || new Date(Date.now() + 1000 * 60 * 5);
const payloadData = LoginPayloadDataSchema.parse({
domain,
address: signerAddress,
nonce: parsedOptions?.nonce,
expiration_time: expirationTime,
});
const message = this.generateMessage(payloadData);
const signature = await this.wallet.sign(message);
return {
payload: payloadData,
signature,
};
}
/**
* Verify Logged In Address
* @remarks Server-side function to securely verify the address of the logged in client-side wallet
* by validating the provided client-side login request.
*
* @param domain - The domain of the server-side application to verify the login request for
* @param payload - The login payload to verify
* @returns The public key of the logged in wallet
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
*
* // Verify the login request
* const address = sdk.auth.verify(domain, loginPayload);
* ```
*/
public verify(domain: string, payload: LoginPayload): string {
// Check that the intended domain matches the domain of the payload
if (payload.payload.domain !== domain) {
throw new Error(
`Expected domain '${domain}' does not match domain on payload '${payload.payload.domain}'`,
);
}
// Check that the payload hasn't expired
const currentTime = new Date();
if (currentTime > new Date(payload.payload.expiration_time)) {
throw new Error(`Login request has expired`);
}
// Check that the signing address is the claimed wallet address
const message = this.generateMessage(payload.payload);
const isValid = this.wallet.verifySignature(
message,
payload.signature,
payload.payload.address,
);
if (!isValid) {
throw new Error(
`Signer address '${payload.payload.address}' did not sign the provided message`,
);
}
return payload.payload.address;
}
/**
* Generate Authentication Token
* @remarks Server-side function that generates a JWT token from the provided login request that the
* client-side wallet can use to authenticate to the server-side application.
*
* @param domain - The domain of the server-side application to authenticate to
* @param payload - The login payload to authenticate with
* @param options - Optional configuration options for the authentication request
* @returns A authentication token that can be used by the client to make authenticated requests
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
*
* // Generate a JWT token that can be sent to the client-side wallet and used for authentication
* const token = await sdk.auth.generateAuthToken(domain, loginPayload);
* ```
*/
public async generateAuthToken(
domain: string,
payload: LoginPayload,
options?: AuthenticationOptions,
): Promise<string> {
if (isBrowser()) {
throw new Error(
"Authentication tokens should not be generated in the browser, as they must be signed by a server-side admin wallet.",
);
}
const parsedOptions = AuthenticationOptionsSchema.parse(options);
const userAddress = this.verify(domain, payload);
const adminAddress = this.wallet.getAddress();
const payloadData = AuthenticationPayloadDataSchema.parse({
iss: adminAddress,
sub: userAddress,
aud: domain,
nbf: parsedOptions?.invalidBefore || new Date(),
exp:
parsedOptions?.expirationTime ||
new Date(Date.now() + 1000 * 60 * 60 * 5),
iat: new Date(),
});
const message = JSON.stringify(payloadData);
const signature = await this.wallet.sign(message);
// Header used for JWT token specifying hash algorithm
const header = {
// Specify ECDSA with SHA-256 for hashing algorithm
alg: "ES256",
typ: "JWT",
};
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
"base64",
);
const encodedData = Buffer.from(JSON.stringify(payloadData))
.toString("base64")
.replace(/=/g, "");
const encodedSignature = Buffer.from(signature).toString("base64");
// Generate a JWT token with base64 encoded header, payload, and signature
const token = `${encodedHeader}.${encodedData}.${encodedSignature}`;
return token;
}
/**
* Authenticate With Token
* @remarks Server-side function that authenticates the provided JWT token. This function verifies that
* the provided authentication token is valid and returns the address of the authenticated wallet.
*
* @param domain - The domain of the server-side application doing authentication
* @param token - The authentication token being used
* @returns The address of the authenticated wallet
*
* @example
* ```javascript
* const domain = "example.com";
* const loginPayload = await sdk.auth.login(domain);
* const token = await sdk.auth.generateAuthToken(domain, loginPayload);
*
* // Authenticate the token and get the address of authenticating users wallet
* const address = sdk.auth.authenticate(domain, token);
* ```
*/
public async authenticate(domain: string, token: string): Promise<string> {
if (isBrowser()) {
throw new Error(
"Should not authenticate tokens in the browser, as they must be verified by the server-side admin wallet.",
);
}
const encodedPayload = token.split(".")[1];
const encodedSignature = token.split(".")[2];
const payload: AuthenticationPayloadData = JSON.parse(
Buffer.from(encodedPayload, "base64").toString(),
);
const signature = Buffer.from(encodedSignature, "base64").toString();
// Check that the token audience matches the domain
if (payload.aud !== domain) {
throw new Error(
`Expected token to be for the domain '${domain}', but found token with domain '${payload.aud}'`,
);
}
// Check that the token is past the invalid before time
const currentTime = Math.floor(new Date().getTime() / 1000);
if (currentTime < payload.nbf) {
throw new Error(
`This token is invalid before epoch time '${payload.nbf}', current epoch time is '${currentTime}'`,
);
}
// Check that the token hasn't expired
if (currentTime > payload.exp) {
throw new Error(
`This token expired at epoch time '${payload.exp}', current epoch time is '${currentTime}'`,
);
}
// Check that the connected wallet matches the token issuer
const connectedAddress = this.wallet.getAddress();
if (connectedAddress?.toLowerCase() !== payload.iss.toLowerCase()) {
throw new Error(
`Expected the connected wallet address '${connectedAddress}' to match the token issuer address '${payload.iss}'`,
);
}
// Check that the connected wallet signed the token
const isValid = this.wallet.verifySignature(
JSON.stringify(payload),
signature,
connectedAddress,
);
if (!isValid) {
throw new Error(
`The connected wallet address '${connectedAddress}' did not sign the token`,
);
}
return payload.sub;
}
/**
* Generates a SIWS compliant message to sign based on the login payload
*/
private generateMessage(payload: LoginPayloadData): string {
let message = ``;
// Add the domain and login address for transparency
message += `${payload.domain} wants you to sign in with your account:\n${payload.address}\n\n`;
// Prompt user to make sure domain is correct to prevent phishing attacks
message += `Make sure that the requesting domain above matches the URL of the current website.\n\n`;
message += `Nonce: ${payload.nonce}\n`;
message += `Expiration Time: ${payload.expiration_time}\n`;
return message;
}
}
-1
View File
@@ -1,5 +1,4 @@
// classes
export * from "./classes/wallet-authenticator";
export * from "./classes/user-wallet";
export * from "./classes/deployer";
// contracts
+9 -6
View File
@@ -1,7 +1,6 @@
import { Deployer } from "./classes/deployer";
import { Registry } from "./classes/registry";
import { UserWallet } from "./classes/user-wallet";
import { WalletAuthenticator } from "./classes/wallet-authenticator";
import { NFTCollection } from "./programs/nft-collection";
import { NFTDrop } from "./programs/nft-drop";
import type { Program } from "./programs/program";
@@ -85,10 +84,6 @@ export class ThirdwebSDK {
* Manage and get info about the connected wallet
*/
public wallet: UserWallet;
/**
* Enable wallet-based server-side authentication
*/
public auth: WalletAuthenticator;
/**
* The currently connected network
*/
@@ -115,11 +110,19 @@ export class ThirdwebSDK {
this.storage = storage;
this.metaplex = Metaplex.make(this.connection);
this.wallet = new UserWallet(this.metaplex);
this.auth = new WalletAuthenticator(this.wallet);
this.registry = new Registry(this.metaplex, this.wallet);
this.deployer = new Deployer(this.registry, this.metaplex, this.storage);
}
get auth() {
throw new Error(
`The sdk.auth namespace has been moved to the @thirdweb-dev/auth package and is no longer available after @thirdweb-dev/sdk >= 3.7.0.
Please visit https://portal.thirdweb.com/auth for instructions on how to switch to using the new auth package (@thirdweb-dev/[email protected]).
If you still want to use the old @thirdweb-dev/[email protected] package, you can downgrade the SDK to version 3.6.0.`,
);
}
/**
* Get an SDK interface for an NFT Collection program
* @param address - Address of the program
-153
View File
@@ -1,153 +0,0 @@
import { AddressSchema, RawDateSchema } from "./common";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
/**
* @internal
*/
export const LoginOptionsSchema = z
.object({
/**
* The optional nonce of the login request used to prevent replay attacks
*/
nonce: z.string().optional(),
/**
* The optional time after which the login payload will be invalid
*/
expirationTime: z.date().optional(),
})
.optional();
/**
* @internal
*/
export const LoginPayloadDataSchema = z.object({
/**
* The domain that the user is attempting to login to
*/
domain: z.string(),
/**
* The public key of the account that is logging in
*/
address: AddressSchema,
/**
* The nonce of the login request used to prevent replay attacks, defaults to a random UUID
*/
nonce: z.string().default(uuidv4()),
/**
* The time after which the login payload will be invalid, defaults to 5 minutes from now
*/
expiration_time: z.date().transform((d) => d.toISOString()),
});
/**
* @internal
*/
export const LoginPayloadSchema = z.object({
/**
* The payload data used for login
*/
payload: LoginPayloadDataSchema,
/**
* The signature of the login request used for verification
*/
signature: z.string(),
});
/**
* @internal
*/
export const AuthenticationOptionsSchema = z
.object({
/**
* The date before which the authentication payload is invalid
*/
invalidBefore: z.date().optional(),
/**
* The date after which the authentication payload is invalid
*/
expirationTime: z.date().optional(),
})
.optional();
/**
* @internal
*/
export const AuthenticationPayloadDataSchema = z.object({
/**
* The address of the wallet issuing the payload
*/
iss: z.string(),
/**
* The address of the wallet requesting to authenticate
*/
sub: z.string(),
/**
* The domain intended to receive the authentication payload
*/
aud: z.string(),
/**
* The date before which the authentication payload is invalid
*/
exp: RawDateSchema,
/**
* The date after which the authentication payload is invalid
*/
nbf: RawDateSchema,
/**
* The date on which the payload was issued
*/
iat: RawDateSchema,
/**
* The unique identifier of the payload
*/
jti: z.string().default(uuidv4()),
});
/**
* @internal
*/
export const AuthenticationPayloadSchema = z.object({
/**
* The payload data used for authentication
*/
payload: AuthenticationPayloadDataSchema,
/**
* The signature of the authentication payload used for authentication
*/
signature: z.string(),
});
/**
* @public
*/
export type LoginOptions = z.input<typeof LoginOptionsSchema>;
/**
* @public
*/
export type LoginPayloadData = z.output<typeof LoginPayloadDataSchema>;
/**
* @public
*/
export type LoginPayload = z.output<typeof LoginPayloadSchema>;
/**
* @public
*/
export type AuthenticationOptions = z.input<typeof AuthenticationOptionsSchema>;
/**
* @public
*/
export type AuthenticationPayloadData = z.output<
typeof AuthenticationPayloadDataSchema
>;
/**
* @public
*/
export type AuthenticationPayload = z.output<
typeof AuthenticationPayloadSchema
>;
-175
View File
@@ -1,175 +0,0 @@
import { signers, sdk } from "./before-setup";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
describe("Wallet Authentication", async () => {
let adminWallet: SignerWithAddress,
signerWallet: SignerWithAddress,
attackerWallet: SignerWithAddress;
const domain = "thirdweb.com";
before(async () => {
[adminWallet, signerWallet, attackerWallet] = signers;
});
beforeEach(async () => {
sdk.updateSignerOrProvider(signerWallet);
});
it("Should verify logged in wallet", async () => {
const payload = await sdk.auth.login(domain);
sdk.updateSignerOrProvider(adminWallet);
const address = sdk.auth.verify(domain, payload);
expect(address).to.equal(signerWallet.address);
});
it("Should verify logged in wallet with chain ID and expiration", async () => {
const payload = await sdk.auth.login(domain, {
expirationTime: new Date(Date.now() + 1000 * 60 * 5),
chainId: 137,
});
sdk.updateSignerOrProvider(adminWallet);
const address = sdk.auth.verify(domain, payload, {
chainId: 137,
});
expect(address).to.equal(signerWallet.address);
});
it("Should reject payload with incorrect domain", async () => {
const payload = await sdk.auth.login(domain);
sdk.updateSignerOrProvider(adminWallet);
try {
sdk.auth.verify("test.thirdweb.com", payload);
expect.fail();
} catch (err) {
expect(err.message).to.equal(
"Expected domain 'test.thirdweb.com' does not match domain on payload 'thirdweb.com'",
);
}
});
it("Should reject expired login payload", async () => {
const payload = await sdk.auth.login(domain, {
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
sdk.updateSignerOrProvider(adminWallet);
try {
sdk.auth.verify(domain, payload);
expect.fail();
} catch (err) {
expect(err.message).to.equal("Login request has expired");
}
});
it("Should reject payload with incorrect chain ID", async () => {
const payload = await sdk.auth.login(domain, {
chainId: 1,
});
sdk.updateSignerOrProvider(adminWallet);
try {
sdk.auth.verify(domain, payload, {
chainId: 137,
});
expect.fail();
} catch (err) {
expect(err.message).to.equal(
"Chain ID '137' does not match payload chain ID '1'",
);
}
});
it("Should reject payload with incorrect signer", async () => {
const payload = await sdk.auth.login(domain);
payload.payload.address = attackerWallet.address;
sdk.updateSignerOrProvider(adminWallet);
try {
sdk.auth.verify(domain, payload);
expect.fail();
} catch (err) {
expect(err.message).to.contain("does not match payload address");
}
});
it("Should generate valid authentication token", async () => {
const payload = await sdk.auth.login(domain);
sdk.updateSignerOrProvider(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload);
const address = await sdk.auth.authenticate(domain, token);
expect(address).to.equal(signerWallet.address);
});
it("Should reject token with incorrect domain", async () => {
const payload = await sdk.auth.login(domain);
sdk.updateSignerOrProvider(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload);
try {
await sdk.auth.authenticate("test.thirdweb.com", token);
expect.fail();
} catch (err) {
expect(err.message).to.contain(
"Expected token to be for the domain 'test.thirdweb.com', but found token with domain 'thirdweb.com'",
);
}
});
it("Should reject token before invalid before", async () => {
const payload = await sdk.auth.login(domain);
sdk.updateSignerOrProvider(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload, {
invalidBefore: new Date(Date.now() + 1000 * 60 * 5),
});
try {
await sdk.auth.authenticate(domain, token);
expect.fail();
} catch (err) {
expect(err.message).to.contain("This token is invalid before");
}
});
it("Should reject expired authentication token", async () => {
const payload = await sdk.auth.login(domain);
sdk.updateSignerOrProvider(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload, {
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
try {
await sdk.auth.authenticate(domain, token);
expect.fail();
} catch (err) {
expect(err.message).to.contain("This token expired");
}
});
it("Should reject if admin address is not connected wallet address", async () => {
const payload = await sdk.auth.login(domain);
sdk.updateSignerOrProvider(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload);
sdk.updateSignerOrProvider(signerWallet);
try {
await sdk.auth.authenticate(domain, token);
expect.fail();
} catch (err) {
expect(err.message).to.contain(
`Expected the connected wallet address '${signerWallet.address}' to match the token issuer address '${adminWallet.address}'`,
);
}
});
});
-139
View File
@@ -1,139 +0,0 @@
import { sdk } from "./before-setup";
import { Keypair } from "@solana/web3.js";
import { expect } from "chai";
describe("Wallet Authentication", async () => {
let adminWallet = Keypair.generate();
let signerWallet = Keypair.generate();
let attackerWallet = Keypair.generate();
const domain = "thirdweb.com";
beforeEach(async () => {
sdk.wallet.connect(signerWallet);
});
it("Should verify logged in wallet", async () => {
const payload = await sdk.auth.login(domain);
sdk.wallet.connect(adminWallet);
const address = sdk.auth.verify(domain, payload);
expect(address).to.equal(signerWallet.publicKey.toBase58());
});
it("Should reject payload with incorrect domain", async () => {
const payload = await sdk.auth.login(domain);
sdk.wallet.connect(adminWallet);
try {
sdk.auth.verify("test.thirdweb.com", payload);
expect.fail();
} catch (err) {
expect(err.message).to.equal(
"Expected domain 'test.thirdweb.com' does not match domain on payload 'thirdweb.com'",
);
}
});
it("Should reject expired login payload", async () => {
const payload = await sdk.auth.login(domain, {
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
sdk.wallet.connect(adminWallet);
try {
sdk.auth.verify(domain, payload);
expect.fail();
} catch (err) {
expect(err.message).to.equal("Login request has expired");
}
});
it("Should reject payload with incorrect signer", async () => {
const payload = await sdk.auth.login(domain);
payload.payload.address = attackerWallet.publicKey.toBase58();
sdk.wallet.connect(adminWallet);
try {
sdk.auth.verify(domain, payload);
expect.fail();
} catch (err) {
expect(err.message).to.contain("did not sign the provided message");
}
});
it("Should generate valid authentication token", async () => {
const payload = await sdk.auth.login(domain);
sdk.wallet.connect(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload);
const address = await sdk.auth.authenticate(domain, token);
expect(address).to.equal(signerWallet.publicKey.toBase58());
});
it("Should reject token with incorrect domain", async () => {
const payload = await sdk.auth.login(domain);
sdk.wallet.connect(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload);
try {
await sdk.auth.authenticate("test.thirdweb.com", token);
expect.fail();
} catch (err) {
expect(err.message).to.contain(
"Expected token to be for the domain 'test.thirdweb.com', but found token with domain 'thirdweb.com'",
);
}
});
it("Should reject token before invalid before", async () => {
const payload = await sdk.auth.login(domain);
sdk.wallet.connect(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload, {
invalidBefore: new Date(Date.now() + 1000 * 60 * 5),
});
try {
await sdk.auth.authenticate(domain, token);
expect.fail();
} catch (err) {
expect(err.message).to.contain("This token is invalid before");
}
});
it("Should reject expired authentication token", async () => {
const payload = await sdk.auth.login(domain);
sdk.wallet.connect(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload, {
expirationTime: new Date(Date.now() - 1000 * 60 * 5),
});
try {
await sdk.auth.authenticate(domain, token);
expect.fail();
} catch (err) {
expect(err.message).to.contain("This token expired");
}
});
it("Should reject if admin address is not connected wallet address", async () => {
const payload = await sdk.auth.login(domain);
sdk.wallet.connect(adminWallet);
const token = await sdk.auth.generateAuthToken(domain, payload);
sdk.wallet.connect(signerWallet);
try {
await sdk.auth.authenticate(domain, token);
expect.fail();
} catch (err) {
expect(err.message).to.contain(
`Expected the connected wallet address '${signerWallet.publicKey.toBase58()}' to match the token issuer address '${adminWallet.publicKey.toBase58()}'`,
);
}
});
});
@@ -2,14 +2,12 @@
import { CoinbasePayIntegration, FundWalletOptions } from "@thirdweb-dev/pay";
import { ChainOrRpc, ThirdwebSDK, getRpcUrl } from "@thirdweb-dev/sdk";
import { ThirdwebStorage } from "@thirdweb-dev/storage";
import {
CoinbaseWallet,
MetaMask,
WalletConnect,
InjectedWallet,
MagicAuthWallet,
} from "@thirdweb-dev/wallets";
import type { AbstractWallet } from "@thirdweb-dev/wallets/dist/declarations/src/wallets/base";
import type { AbstractBrowserWallet } from "@thirdweb-dev/wallets/evm/wallets/base";
import { CoinbaseWallet } from "@thirdweb-dev/wallets/evm/wallets/coinbase-wallet";
import { InjectedWallet } from "@thirdweb-dev/wallets/evm/wallets/injected";
import { MagicAuthWallet } from "@thirdweb-dev/wallets/evm/wallets/magic-auth";
import { MetaMask } from "@thirdweb-dev/wallets/evm/wallets/metamask";
import { WalletConnect } from "@thirdweb-dev/wallets/evm/wallets/wallet-connect";
import { BigNumber } from "ethers";
import type { ContractInterface, Signer } from "ethers";
@@ -65,8 +63,8 @@ interface TWBridge {
const w = window;
class ThirdwebBridge implements TWBridge {
private walletMap: Map<string, AbstractWallet> = new Map();
private activeWallet: AbstractWallet | undefined;
private walletMap: Map<string, AbstractBrowserWallet> = new Map();
private activeWallet: AbstractBrowserWallet | undefined;
private initializedChain: ChainOrRpc | undefined;
private activeSDK: ThirdwebSDK | undefined;
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-coinbase-wallet.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-coinbase-wallet.esm.js"
}
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-injected.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-injected.esm.js"
}
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-magic.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-magic.esm.js"
}
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-metamask.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-metamask.esm.js"
}
@@ -1,4 +0,0 @@
{
"main": "dist/thirdweb-dev-wallets-connectors-wallet-connect.cjs.js",
"module": "dist/thirdweb-dev-wallets-connectors-wallet-connect.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-evm-connectors-coinbase-wallet.cjs.js",
"module": "dist/thirdweb-dev-wallets-evm-connectors-coinbase-wallet.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-evm-connectors-injected.cjs.js",
"module": "dist/thirdweb-dev-wallets-evm-connectors-injected.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-evm-connectors-magic.cjs.js",
"module": "dist/thirdweb-dev-wallets-evm-connectors-magic.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-evm-connectors-metamask.cjs.js",
"module": "dist/thirdweb-dev-wallets-evm-connectors-metamask.esm.js"
}
@@ -0,0 +1,4 @@
{
"main": "dist/thirdweb-dev-wallets-evm-connectors-wallet-connect.cjs.js",
"module": "dist/thirdweb-dev-wallets-evm-connectors-wallet-connect.esm.js"
}

Some files were not shown because too many files have changed in this diff Show More