In this guide, we'll show you how to create an app where users can sign in with their GitHub and web3 wallet. Once they are signed in, we will validate if they have contributed to one of our repos; and if they have, we will allow the users to mint an NFT using signature-based minting!
Before we get started, below are some helpful resources where you can learn more about the tools we're going to be using in this guide.
Let's get started!
Setup
I am going to use the Next.js Typescript starter template for this guide.
If you are following along with the guide, you can create a project with the template using the thirdweb CLI:
npx thirdweb create --next --ts
If you already have a Next.js app you can simply follow these steps to get started:
Install
@thirdweb-dev/react
and@thirdweb-dev/sdk
andethers
Add MetaMask authentication to the site. You can follow this guide to add metamask auth.
By default the network is Mainnet, we need to change it to Goerli.
import type { AppProps } from "next/app";
import { ChainId, ThirdwebProvider } from "@thirdweb-dev/react";
// This is the chainId your dApp will work on.
const activeChainId = ChainId.Goerli;
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThirdwebProvider desiredChainId={activeChainId}>
<Component {...pageProps} />
</ThirdwebProvider>
);
}
export default MyApp;
Creating an Edition Contract and Lazy-Minting An NFT
Let's go ahead and deploy an Edition contract and add an NFT!
To do that, head to the thirdweb dashboard and create an Edition contract!
Fill out the details and deploy the contract by clicking on Deploy Now
.
Once the contract is deployed, go to the NFTs tab and click on Mint. Enter the details of your NFT and set the initial supply to 0. Once you have filled out all the details, click on Mint NFT!
We only want users to own 1
NFT per wallet from this collection. To achieve this, we'll set our NFTs to be soulbound, meaning they cannot transfer it!
Head to the permissions tab and set the NFTs of this collection to be Non-Transferrable.
Setting up Auth
We need the user to sign in both with their wallet and with their GitHub account, to do this, we'll first set up web3 sign-in using thirdweb then use NextAuth to add GitHub authentication!
thirdweb Auth
Firstly, we need to install the thirdweb auth package:
npm i @thirdweb-dev/auth # npm
yarn add @thirdweb-dev/auth # yarn
Now, create a file called auth.config.ts and the following:
import { ThirdwebAuth } from "@thirdweb-dev/auth/next";
export const { ThirdwebAuthHandler, getUser } = ThirdwebAuth({
// Using environment variables to secure your private key is a security vulnerability.
// Learn how to store your private key securely:
// https://portal.thirdweb.com/sdk/set-up-the-sdk/securing-your-private-key
process.env.ADMIN_PRIVATE_KEY!,
// Set this to your domain to prevent signature malleability attacks.
const domain = "http://localhost:3000";
});
The domain is important since you don't want your users signing malicious signatures
Create a new
.env.local
file and add a new variable namedPRIVATE_KEY
. Learn how to export your private key from your wallet. Using private keys as an env variable is vulnerable to attacks and is not the best practice. We are doing it in this guide for the sake of brevity, but we strongly recommend using a secret manager to store your private key.
To configure the auth API, create a new folder inside pages/api
called thirdwebauth
and a [...thirdweb].ts
file inside it!
Within this file, we need to export the ThirdwebHandler
that we made in the config file previously.
import { ThirdwebAuthHandler } from "../../../auth.config";
export default ThirdwebAuthHandler();
This will catch all the API requests that go to /api/auth/...
, using Next.js's catch-all API route syntax.
Behind the scenes, this handles the logic for three endpoints that we're going to utilize: /login
, /logout
, and /user
.
Finally, inside the _app.tsx
file, add the authConfig
prop to ThirdwebProvider
:
<ThirdwebProvider
desiredChainId={activeChainId}
authConfig={{
authUrl: "/api/thirdwebauth",
domain: "example.org",
loginRedirect: "/",
}}
>
<Component {...pageProps} />
</ThirdwebProvider>
Next Auth with GitHub
Create a new folder called auth
inside the pages/api
folder and then create a file inside auth
called [...nextauth].ts
. Once you have done this add the following to the file:
import NextAuth, { NextAuthOptions } from "next-auth";
import GithubProvider from "next-auth/providers/github";
export const authOptions: NextAuthOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID as string,
clientSecret: process.env.GITHUB_SECRET as string,
}),
],
callbacks: {
async session({ session, token }) {
session.id = token.sub;
return session;
},
},
};
export default NextAuth(authOptions);
Now, go to _app.tsx
and wrap the component in a session provider like this:
<SessionProvider session={pageProps.session} refetchInterval={0}>
<Component {...pageProps} />
</SessionProvider>
The SessionProvider
will be imported from next-auth like this:
import { SessionProvider } from "next-auth/react";
We need to add some env variables to make the auth work! In the .env.local
file add these variables:
GITHUB_ID=
GITHUB_SECRET=
NEXTAUTH_URL=
NEXTAUTH_SECRET=
The NEXTAUTH_URL
will be a link to the homepage of your website, since we are working on localhost it will be http://localhost:3000
.
To generate a secret run the following command:
openssl rand -base64 32
Now, we need to create a GitHub OAuth app. You can do that from the Github Developer Settings and fill out the information like so:
Now, copy the client id and generate a new secret and copy that. Go to your .env.local
file and update these variables. Once you have filled all the env vars restart your dev server!
Next, follow Github's Creating a personal access token guide and add this as an environment variable in your project. We'll use this to make requests to GitHub's API without getting rate-limited.
GITHUB_ACCESS_TOKEN=
Creating the login page
Create a new file login.tsx
inside the pages
folder and add the following:
import React from "react";
import { useSession, signIn } from "next-auth/react";
import styles from "../styles/Home.module.css";
const Login = () => {
const { data: session } = useSession();
if (!session) {
return (
<div className={styles.container}>
<button className={styles.mainButton} onClick={() => signIn()}>
Sign in with GitHub
</button>
</div>
);
}
};
export default Login;
This will show a Sign in with GitHub button if there is no session detected from NextAuth.
Now, we will check for the address and thirdweb user. If the user's wallet is not connected/the user isn't signed in we will show the corresponding buttons.
Firstly, we are going to need 3 hooks:
const login = useLogin();
const address = useAddress();
const connect = useMetamask();
Below the session check add the following:
if (!address) {
return (
<div className={styles.container}>
<button className={styles.mainButton} onClick={() => connect()}>
Connect Wallet
</button>
</div>
);
}
return (
<div className={styles.container}>
<button className={styles.mainButton} onClick={() => login()}>
Sign in with Ethereum
</button>
</div>
);
This code block will first check if there is an address present. If there is no address present it will show the connect wallet
button; otherwise, the sign in with Ethereum
button.
We are using css modules to add some basic stylings to the app, so if you want to add the stylings as well. Create a new folder styles
and globals.css
inside it
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
Now, import it in _app.tsx like this:
import "../styles/globals.css";
Now, create another file called Home.module.css and the following:
.container {
width: 100%;
height: 70vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.mainButton {
background: linear-gradient(to right, #ff1b6b, #45caff);
border: none;
color: #fff;
font-weight: 700;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 5px;
}
Now, we have the auth flow completed! You can go to http://localhost:3000/login and check it out yourself.
Creating the API for claim
Create a new file claim-nft.ts
inside the pages/api
folder. We are going to need to create an api so add the following:
import type { NextApiRequest, NextApiResponse } from "next";
const claimNft = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
return res.status(200).json({ message: "gm" });
};
export default claimNft;
This api first checks if the request is of the post method or not and if it is using the post method we will send a message "gm".
Now, let's get into the actual code! We will first get the users from the next auth and thirdweb auth like this:
const session = await unstable_getServerSession(req, res, authOptions);
const thirdwebUser = await getUser(req);
if (!session || !thirdwebUser) {
return res.status(401).json({ message: "Unauthorized" });
}
We need to import them like this:
import { unstable_getServerSession } from "next-auth/next";
import { getUser } from "../../auth.config";
These are the two functions we use to view the authenticated user's information on the server-side:
-
unstable_getServerSession
is our way of getting the connected Github account -
getUser
is our way of viewing the authenticated wallet. Now, we will create a new array that will have a list of repos to check for:
const reposToCheck = [
"js",
"react",
"typescript-sdk",
"thirdweb-cli",
"contracts",
"auth",
"storage",
"go-sdk",
"python-sdk",
"docs",
"portal",
"examples",
];
Here, we have added some of our thirdweb repositories, but you need to update this with your list of repositories to check.
Now, we will loop through this array and for each item we will check if the user has contributed to them:
let hasContributed = false;
for (const repo of reposToCheck) {
const contributors: GithubContributor[] = await fetch(
`https://api.github.com/repos/thirdweb-dev/${repo}/contributors`,
{
headers: {
Authorization: `token ${process.env.GITHUB_ACCESS_TOKEN}`,
},
}
).then((res) => res.json());
const hasContributedToThisRepo = contributors?.some((contributor) => {
return contributor.id.toString() === session.id;
});
if (hasContributedToThisRepo) {
hasContributed = true;
}
}
Now that we know if the user has contributed or not, if the user has not contributed we will simply say that you don't qualify:
if (!hasContributed) {
return res.status(401).json({ message: "Sorry, you don't qualify" });
}
Now, let's initialize our SDK using our private key:
const sdk = ThirdwebSDK.fromPrivateKey(
// Using environment variables to secure your private key is a security vulnerability.
// Learn how to store your private key securely:
// https://portal.thirdweb.com/sdk/set-up-the-sdk/securing-your-private-key
process.env.PRIVATE_KEY as string,
"goerli"
);
We will first check if the user already owns one of our NFTs first:
const edition = sdk.getEdition("0xD71c27e6325f018b15E16C3992654F1b089C5fCe");
const balance = await edition.balanceOf(thirdwebUser.address, 0);
if (balance.gt(0)) {
return res.status(401).json({ message: "You already have an NFT" });
}
Finally, we will generate the signature to mint the NFT!
const signedPayload = await edition.signature.generateFromTokenId({
quantity: 1,
tokenId: 0,
to: thirdwebUser.address,
});
return res.status(200).json({ signedPayload });
Our final API code will looks like this:
import type { NextApiRequest, NextApiResponse } from "next";
import { unstable_getServerSession } from "next-auth/next";
import { getUser } from "../../auth.config";
import GithubContributor from "../../types/GithubContributor";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { authOptions } from "./auth/[...nextauth]";
const claimNft = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const session = await unstable_getServerSession(req, res, authOptions);
const thirdwebUser = await getUser(req);
if (!session || !thirdwebUser) {
return res.status(401).json({ message: "Unauthorized" });
}
const reposToCheck = [
"js",
"react",
"typescript-sdk",
"thirdweb-cli",
"contracts",
"auth",
"storage",
"go-sdk",
"python-sdk",
"docs",
"portal",
"examples",
];
let hasContributed = false;
for (const repo of reposToCheck) {
const contributors: GithubContributor[] = await fetch(
`https://api.github.com/repos/thirdweb-dev/${repo}/contributors`,
{
headers: {
Authorization: `token ${process.env.GITHUB_ACCESS_TOKEN}`,
},
}
).then((res) => res.json());
const hasContributedToThisRepo = contributors?.some((contributor) => {
return contributor.id.toString() === session.id;
});
if (hasContributedToThisRepo) {
hasContributed = true;
}
}
if (!hasContributed) {
return res.status(401).json({ message: "Sorry, you don't qualify" });
}
const sdk = ThirdwebSDK.fromPrivateKey(
process.env.PRIVATE_KEY as string,
"goerli"
);
const edition = sdk.getEdition("0xD71c27e6325f018b15E16C3992654F1b089C5fCe");
const balance = await edition.balanceOf(thirdwebUser.address, 0);
if (balance.gt(0)) {
return res.status(401).json({ message: "You already have an NFT" });
}
const signedPayload = await edition.signature.generateFromTokenId({
quantity: 1,
tokenId: 0,
to: thirdwebUser.address,
});
return res.status(200).json({ signedPayload });
};
export default claimNft;
Creating the frontend
Calling the API on the frontend
Go to pages/index.tsx
and update the return block:
return (
<div className={styles.container}>
<h1>GitHub Contributor NFTs</h1>
<p>
Claim an NFT if you have contributed to any of thirdweb's repos.
</p>
{!address ? (
<button
className={styles.mainButton}
disabled={loading}
onClick={() => connect()}
>
Connect to Metamask
</button>
) : (
<button
className={styles.mainButton}
disabled={loading}
onClick={mintNft}
>
{loading ? "Loading..." : "Claim NFT"}
</button>
)}
</div>
);
We are going to check if the user's wallet is connected and if it is then we will show a button to claim the NFT!
As you can see we are going to need a mintNFT
function so let's create that:
const mintNft = async () => {
setLoading(true);
if (networkMismatch) {
return switchNetwork?.(ChainId.Goerli);
}
try {
const req = await fetch("/api/claim-nft", {
method: "POST",
});
const res = await req.json();
if (!req.ok) {
return alert(`Error: ${res.message}`);
}
await edition?.signature.mint(res.signedPayload);
alert("Successfully minted NFT 🚀");
} catch (err) {
console.error(err);
alert("Failed to mint NFT");
} finally {
setLoading(false);
}
};
We are also using some hooks to access the edition contract, get the user's address, connect the wallet to MetaMask, store loading, etc.:
const edition = useEdition("0xD71c27e6325f018b15E16C3992654F1b089C5fCe");
const connect = useMetamask();
const address = useAddress();
const [, switchNetwork] = useNetwork();
const networkMismatch = useNetworkMismatch();
const [loading, setLoading] = useState<boolean>(false);
Now, if you have contributed to any of the repositories you will successfully be able to mint the NFT! 🎉
Redirecting users if they are not logged in
If the user is not logged in and they come to the home page we should redirect them to /login
so, we are going to use SSR to redirect the user if they are not logged in:
export const getServerSideProps: GetServerSideProps = async (context) => {
const thirdwebUser = await getUser(context.req);
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
);
if (!thirdwebUser || !session) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
return {
props: {},
};
};
Conclusion
In this guide, we learnt how to use thirdweb auth to allow only users who have contributed to our repos to mint an NFT!
If you did as well pat yourself on the back and share it with us on the thirdweb discord! If you want to take a look at the code, check out the GitHub Repository.
Top comments (3)
Why?
improving content quality and choosing new topic to discus is great concept . vashikaran mantra for husband in Hind
Why?