[Auth] complete auth update & restructure (#460)
This commit is contained in:
@@ -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.
|
||||
@@ -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();
|
||||
```
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@thirdweb-dev/unity-js-bridge": minor
|
||||
---
|
||||
|
||||
Upgrade with new wallets for major Auth upgrade
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-evm.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-evm.esm.js"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-next-evm.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-next-evm.esm.js"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-next-solana.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-next-solana.esm.js"
|
||||
}
|
||||
+59
-22
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-auth-solana.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth-solana.esm.js"
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ThirdwebAuth } from "./auth";
|
||||
export * from "./schema";
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const isBrowser = () => typeof window !== "undefined";
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const isNode = () => !isBrowser();
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./core";
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
+2
-1
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extension": [
|
||||
"ts"
|
||||
],
|
||||
"require": [
|
||||
"@swc-node/register"
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"extends": "@thirdweb-dev/tsconfig/base.json",
|
||||
"extends": "@thirdweb-dev/tsconfig/sdk.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"main": "dist/thirdweb-dev-sdk-evm-wallets.cjs.js",
|
||||
"module": "dist/thirdweb-dev-sdk-evm-wallets.esm.js"
|
||||
}
|
||||
@@ -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 +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,5 +1,4 @@
|
||||
export * from "./types";
|
||||
export * from "./auth";
|
||||
export * from "./classes";
|
||||
export * from "./wallet";
|
||||
export * from "./sdk";
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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";
|
||||
@@ -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,5 +1,4 @@
|
||||
// classes
|
||||
export * from "./classes/wallet-authenticator";
|
||||
export * from "./classes/user-wallet";
|
||||
export * from "./classes/deployer";
|
||||
// contracts
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user