Add "Sign in with Github" in one click to authorize your Github OAuth App and access Github REST API using Cotter and Next.js.
Cotter just launched a Github Login integration š. This means you can easily log your users in and get an access token to enable Github integrations in your app.
What we're building
We're going to build a website with Next.js that allows your users to login with email or with Github and get a list of their public and private repositories.
Overview
- Let's Start - Make our Home Page
- Let's see how this works before moving on to Github API
- Designing our API endpoints to get data from Github
- Showing the Repo List in our Dashboard Page
- But, what if the user didn't Sign in with Github?
Let's Start ā Make our Home Page
Create your Next.js project
Start with creating a new Next.js project by running the code below, and follow the instructions.
yarn create next-app
Add a Login Form in the Home Page
We're using Cotter for the Login form to quickly enable an Email Magic Link login and Sign in with Github.
Add Cotter as a dependency
yarn add cotter
Add a Login Form and a Title
Modify our home page at pages/index.js
. We'll start with the simple Email Magic Link login. Remove everything in pages/index.js
and add a title and Cotter's login form:
import { useEffect } from "react";
import styles from "../styles/Home.module.css";
import Cotter from "cotter"; // 1ļøā£ Import Cotter
import { useRouter } from "next/router";
export default function Home() {
const router = useRouter();
// 2ļøā£ Initialize and show the form
useEffect(() => {
var cotter = new Cotter(API_KEY_ID); // š Specify your API KEY ID here
cotter
.signInWithLink() // use .signInWithOTP() to send an OTP
.showEmailForm() // use .showPhoneForm() to send magic link to a phone number
.then((response) => {
console.log(response); // show the response
router.push("/dashboard");
})
.catch((err) => console.log(err));
}, []);
return (
<div className={styles.container}>
<h1 className={styles.subtitle}>Welcome to my Github App</h1>
{/* 3ļøā£ Put a <div> that will contain the form */}
<div id="cotter-form-container" style={{ width: 300, height: 300 }} />
</div>
);
}
You'll need an API_KEY_ID
, create a new project, and copy the API_KEY_ID
from the dashboard. The code above should give you a simple Login page that looks like this:
Enable Github Login
The documentation laid out the steps you need to take to enable Social Login to your login form. We'll follow it step by step below:
First, create a Github OAuth App. Summarizing Github's documentation, you should do the following:
- Click on your profile picture on the top right > Settings > Developer Settings > OAuth Apps > New OAuth App
- Fill in your Application Name, Homepage URL, and description based on your app.
- Fill in
https://www.cotter.app/api/v0/oauth/token/callback/GITHUB
for the Authorization callback URL. - Then click Register Application.
Go to your Cotter Dashboard and add a Social Login connection.
Go to Dashboard > Social Login > New Login Connection > Github. Then copy your Client ID and Client Secret from Github. We'll add the repo
scope because we want to get the users' repository data.
Press Create to create the login connection.
Show Github Login on your Form
Now that your Social Login connection is set up, we can show it in our Login Form. Go to Dashboard > Branding > Magic Link. Check the box for Github under Social Login Providers.
Press Save to update your customization.
You should now see the Sign in with Github button in our Next.js app.
Let's see how this works before moving on to Github API
We'll go over how the login works, how you can authenticate users to your backend, and how you can get Github's access token to access private repo data.
1. Let's try logging in with your email address first.
Enter your email address and press "Sign in Without Password". Tap the magic link in your email and you should be logged-in.
Now check your console log, you should see something like this:
{
"token": {...},
"email": "team@cotter.app", // š the user's email
"oauth_token": {
"access_token": "eyJhbGciOiJFUzI...", // š access token
"id_token": "eyJhbGciOiJFUzI1...",
"refresh_token": "236:QDVxW6...",
"expires_in": 3600,
"token_type": "Bearer",
"auth_method": "OTP"
},
"user": {
"ID": "abcdefgh-abcd-abcd-9959-67ebae3cdfcf", // š user ID
"issuer": "abcdefgh-abcd-abcd-81ad-5cc8b69051e8",
"identifier": "team@cotter.app",
...
}
}
Note: If your console log can't be expanded because you redirected to
/dashboard
, comment out the redirection partrouter.push("/dashboard")
Three things that you should take note of are the user's email , the Cotter User ID, and the access_token that we will use to protect our API endpoints. These information will be available to you at anytime the user is logged-in by calling cotter.tokenHandler.getAccessToken()
and cotter.getLoggedInUser()
2. Let's try logging-in again but with your Github account that has the same email address
When you used a Github account that has the same address as an existing account, you should see a prompt asking if you'd like to link the accounts:
If you're using a Github account that has an email address that is not recognized, then it will automatically create a new user. You'll see the same JSON response like the above when the user has successfully log in with Github.
Designing our API endpoints to get data from Github
- We will have a dashboard page that will call our API endpoint at
/api/repo
to get a list of repositories owned by the user. - We'll make an API endpoint
/api/repo
that will:
- Check if the user is logged-in
- If logged-in, get the user's Github Access Token from Cotter's API
- Call Github API to get the authenticated user's repository list
Make our API endpoint at /api/repo
Our endpoint will look like this:
GET http://localhost:3000/api/repo
Authorization: Bearer <Cotter Access Token>
1. Make a function to handle API calls to /api/repo
Next.js gives you a neat way to add server code that can handle API requests. To handle an API call to /api/repo
, make a file pages/api/repo.js
. Then, we'll add a skeleton handler function with a list of things that we need to do:
const handler = async (req, res) => {
// TODO: Check if Authorization Header has a valid access_token
// TODO: Parse the access_token to get cotter_user_id to
// TODO: Call Cotter's API to get Github Access Token for the user
// TODO: Call Github API to get the repository data
};
export default handler;
2. Check if the Authorization Header have a valid access token
We'll make a separate function above our handler
function to do this check. We'll be using Cotter's client library to help us validate the access token.
yarn add cotter-node
// 1) Import Cotter
import { CotterValidateJWT } from "cotter-node";
const checkJWT = (handler) => async (req, res) => {
// 2) Check that the access_token exists
if (!("authorization" in req.headers)) {
res.statusCode = 401;
res.end("Authorization header missing");
return;
}
const auth = await req.headers.authorization;
const bearer = auth?.split(" ");
const token = bearer?.length > 0 && bearer[1];
// 3) Validate the access_token
var valid = false;
try {
valid = await CotterValidateJWT(token);
} catch (e) {
console.log(e);
valid = false;
}
if (!valid) {
res.statusCode = 403;
res.end("Authorization header is invalid");
return;
}
// 4) Pass the access token to the next handler
req.access_token = token;
handler(req, res);
};
const handler = async (req, res) => {...};
// 5) We are passing our handler function into
// `checkJWT` so that `checkJWT` will be run first
// before our `handler` is run.
export default checkJWT(handler);
What we did was pretty simple:
- First, we check if the Authorization header exists
- If it exists, then we check if the
access_token
is valid using Cotter's helper function. - Then we call
checkJWT(handler)
to run the check and then run the handler if the check passed.
3. Get the Cotter User ID from the access_token
.
We will need this for our API call to Cotter. The access_token
is a JWT token that contains the user's Cotter User ID. Check here for the full spec. We'll use another Cotter helper function to parse the access token and get the Cotter User ID.
yarn add cotter-token-js
import { CotterValidateJWT } from "cotter-node";
// 1) Import Cotter Token
import { CotterAccessToken } from "cotter-token-js";
const checkJWT = (handler) => async (req, res) => {...};
const handler = async (req, res) => {
// Parse the access_token to get cotter_user_id
const decodedToken = new CotterAccessToken(req.access_token);
const cotterUserID = decodedToken.getID();
// TODO: Call Cotter's API to get Github Access Token for the user
// TODO: Call Github API to get the repository data
};
export default checkJWT(handler);
4. Get Github Access Token from Cotter API
The API to get a Social Provider access token from Cotter looks like this
curl -XGET \
-H 'API_KEY_ID: <COTTER API KEY ID>' \
-H 'API_SECRET_KEY: <COTTER API SECRET KEY>' \
'https://www.cotter.app/api/v0/oauth/token/GITHUB/<COTTER USER ID>'
Let's install axios and create our request
yarn add axios
import axios from "axios"; // Import axios
const checkJWT = (handler) => async (req, res) => {...};
const handler = async (req, res) => {
// Parse the access_token to get cotter_user_id
...
// Call Cotter's API to get Github Access Token for the user
let githubAccessToken = "";
const config = {
headers: {
API_KEY_ID: process.env.COTTER_API_KEY_ID,
API_SECRET_KEY: process.env.COTTER_API_SECRET_KEY,
},
};
try {
let resp = await axios.get(
`https://www.cotter.app/api/v0/oauth/token/GITHUB/${cotterUserID}`,
config
);
githubAccessToken = resp.data.tokens?.access_token;
} catch (err) {
res.statusCode = 500;
res.end("Fail getting Github access token from Cotter API");
return;
}
// TODO: Call Github API to get the repository data
};
export default checkJWT(handler);
As you can see we're storing our secrets in an environment variable. Get your API_KEY_ID
and API_SECRET_KEY
from the dashboard and export it in your terminal, then run yarn dev
.
$ export COTTER_API_KEY_ID=<API KEY ID>
$ export COTTER_API_SECRET_KEY=<API SECRET KEY>
$ yarn dev
5. Call Github API to get the repositories list
Github's API to get the list of repositories of the authenticated user looks like this:
curl \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token <GITHUB ACCESS TOKEN>" \
"https://api.github.com/user/repos"
Let's make the request using axios and the Github Access Token that we get in the earlier step.
const handler = async (req, res) => {
// Parse the access_token to get cotter_user_id to
...
// Call Cotter's API to get Github Access Token for the user
...
// Call Github API to get the repository data
const githubConfig = {
headers: {
Accept: "application/vnd.github.v3+json",
Authorization: `token ${githubAccessToken}`,
},
};
try {
let resp = await axios.get(
`https://api.github.com/user/repos`,
githubConfig
);
// We only want to show the repo name and url
const repoData = resp.data?.map((repo) => ({
full_name: repo.full_name,
url: repo.html_url,
}));
res.statusCode = 200;
res.json(repoData);
return;
} catch (err) {
res.statusCode = 500;
res.end("Fail getting repostories from Github API");
return;
}
};
export default checkJWT(handler);
That's it, let's try our API endpoint
Copy your access token from the console log when logging in and run:
curl \
-H "Authorization: Bearer <COTTER ACCESS TOKEN>" \
"http://localhost:3000/api/repo"
You should see the following response:
[
{
"full_name": "putrikarunia/project1",
"url": "https://github.com/putrikarunia/project1"
},
{
"full_name": "putrikarunia/project2",
"url": "https://github.com/putrikarunia/project2"
},
{
"full_name": "putrikarunia/project3",
"url": "https://github.com/putrikarunia/project3"
}
]
/api/repo
Showing the Repo List in our Dashboard Page
Make the Dashboard Page
Add a dashboard page by making a file at pages/dashboard.js
. Using useEffect
we will call our API endpoint to get the repositories, and put the results in our React state:
import { useEffect, useState } from "react";
import styles from "../styles/Home.module.css";
import Cotter from "cotter";
import axios from "axios";
export default function Dashboard() {
const [err, seterr] = useState(null);
const [repos, setrepos] = useState([]);
// Get a list of repositories
useEffect(() => {
getRepositories();
}, []);
const getRepositories = async () => {
// 1ļøā£ Get Access Token for Logged-in User
var cotter = new Cotter(API_KEY_ID); // š Specify your API KEY ID here
const accessToken = await cotter.tokenHander.getAccessToken();
// 2ļøā£ Make the request to our `/api/repo` endpoint
const config = {
headers: {
Authorization: `Bearer ${accessToken?.token}`,
},
};
try {
let resp = await axios.get("/api/repo", config);
setrepos(resp.data);
} catch (err) {
seterr(JSON.stringify(err.response?.data));
}
};
return (
<div className={styles.container}>
<h1 className={styles.subtitle}>
Welcome! Here's a list of your Github Repos
</h1>
{/* Show any error here */}
<div style={{ color: "#FF0000" }}>{err}</div>
{/* 3ļøā£ Show the list of repositories */}
<div className={styles.main}>
{repos.map((repo) => (
<div className={styles.card}>
<h3>{repo.full_name}</h3>
<a href={repo.url}>{repo.url}</a>
</div>
))}
</div>
</div>
);
}
Let's go over what we did:
- We added 2 React states,
err
andrepos
, to show errors and the repo data. - When the component mounts, we call
getRepositories
which first gets the user's access token using Cotter's functioncotter.tokenHandler.getAccessToken()
, then calls an API request to our backend endpoint at/api/repo
. - When the API call is successful, the function will update our
repos
state with the list of repositories, or show an error.
If you sign-in with Github, then go to localhost:3000/dashboard
, you'll see the following:
Don't forget to uncomment the redirection part in
/pages/index.js
:router.push("/dashboard")
Add a NavBar to Log Out or Log In and navigate between pages
Let's add a Navbar component to help our users navigate our website. Make a file /components/Navbar/index.js
in your project directory.
import { useState, useEffect } from "react";
import Link from "next/link";
import Cotter from "cotter";
export default function Navbar() {
const [loggedIn, setloggedIn] = useState(false);
const [email, setemail] = useState(null);
useEffect(() => {
checkLoggedIn();
}, []);
// TODO: Check if the user is logged-in
const checkLoggedIn = async () => {};
// TODO: Log out the user
const logOut = () => {};
return (
<div style={{ display: "flex", justifyContent: "flex-end" }}>
{loggedIn ? (
<div style={{ padding: 20 }} onClick={logOut}>
Log Out
</div>
) : (
<Link href="/">
<a style={{ padding: 20 }}>Log In</a>
</Link>
)}
{loggedIn && <div style={{ padding: 20 }}>{email}</div>}
<Link href="/dashboard">
<a style={{ padding: 20 }}>Go to Dashboard</a>
</Link>
</div>
);
}
- We added a
loggedIn
andemail
state. If the user is logged-in, we'll display the Log Out button and the user's email, otherwise we'll display the Login button. - The function
checkLoggedIn
will check if the user is logged in and update theloggedIn
state and set the user'semail
state - We also added a function called
logOut
to log out the user.
Make the checkLoggedIn
function
We can do this using Cotter's function by checking if an access token exists. Update your checkLoggedIn
function:
const checkLoggedIn = async () => {
const cotter = new Cotter(API_KEY_ID); // š Specify your API KEY ID here
const accessToken = await cotter.tokenHander.getAccessToken();
if (accessToken?.token.length > 0) {
setloggedIn(true);
const user = cotter.getLoggedInUser();
setemail(user?.identifier);
} else {
setloggedIn(false);
}
};
Make the logOut
function
We can also do this by calling Cotter's cotter.logOut()
function. Update your logOut
function:
const logOut = async () => {
const cotter = new Cotter(API_KEY_ID); // š Specify your API KEY ID here
await cotter.logOut();
setloggedIn(false);
window.location.href = "/";
};
Import the Navbar in your Home Page and Dashboard Page
In /pages/index.js
:
import Navbar from "../components/Navbar";
export default function Home() {
...
return (
<>
<Navbar /> // Add the navbar
<div className={styles.container}>...</div>
</>
);
}
In /pages/dashboard.js
:
import Navbar from "../components/Navbar";
export default function Dashboard() {
...
return (
<>
<Navbar /> // Add the navbar
<div className={styles.container}>...</div>
</>
);
}
Great! Now our website works well and users can log in/log out and get their Repositories list.
But, what if the user didn't Sign in with Github?
If the user didn't Sign in with Github, then we wouldn't get Github's Access Token, and it will return an error like this:
How do we fix this?
Fortunately, Cotter has a function to allow logged-in users to Connect a Github Account of their choosing to their current account. This means we can add a button in the dashboard that tells the user to Connect Github if we get this error.
Add a button to Connect Github if not connected yet.
Following the guide to Connect a Github account to an Existing User, we'll add a function and a button at pages/dashboard.js
import Cotter from "cotter";
export default function Dashboard() {
...
// Get a list of repositories
useEffect(() => {...}, []);
const getRepositories = async () => {...};
const connectToGithub = async () => {
var cotter = new Cotter(API_KEY_ID); // š Specify your API KEY ID here
const accessToken = await cotter.tokenHandler.getAccessToken();
cotter.connectSocialLogin("GITHUB", accessToken?.token); // pass in the provider's name
};
return (
<>
<Navbar />
<div className={styles.container}>
{/* Show any error here */}
...
{/* If there's no Github access token, show a button to connect a Github account */}
{err?.includes("Fail getting Github access token from Cotter API") && (
<div className={styles.card} onClick={connectToGithub}>
Connect Github
</div>
)}
{/* Show the list of repositories */}
...
</div>
</>
);
}
Now let's try logging in with an email that is not associated with your Github account using the Email Address
field. You should see something like this:
Press Connect Github , and it will connect your currently logged-in Github Account with this email address.
If you log out and log in again with Github, you will now be logged-in to this new email address.
How do I disconnect a Github Account
We won't cover this in the tutorial, but you can use our API endpoint to delete a connection.
That's it!
We now have a working Github API integration with a simple way to get your user's Github Access Token.
What's Next?
There's a lot of things that you can do using Github's API.
- Check out the complete list of Github's REST API here.
- See the code for this tutorial in our Github repo.
Questions & Feedback
Come and talk to the founders of Cotter and other developers who are using Cotter on Cotter's Slack Channel.
Ready to use Cotter?
If you enjoyed this tutorial and want to integrate Cotter into your website or app, you can create a free account and check out our documentation.
If you need help, ping us on our Slack channel or email us at team@cotter.app.
Top comments (0)